diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/AbstractWidgetRenderer.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/AbstractWidgetRenderer.java index 2afb5f69..2bb15cfc 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/AbstractWidgetRenderer.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/AbstractWidgetRenderer.java @@ -42,6 +42,7 @@ import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetOutput; import com.kingsrook.qqq.backend.core.model.dashboard.widgets.QWidgetData; import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData; import com.kingsrook.qqq.backend.core.model.metadata.dashboard.WidgetDropdownData; +import com.kingsrook.qqq.backend.core.model.metadata.dashboard.WidgetDropdownType; import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValue; import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; @@ -72,80 +73,104 @@ public abstract class AbstractWidgetRenderer *******************************************************************************/ protected boolean setupDropdowns(RenderWidgetInput input, QWidgetMetaData metaData, QWidgetData widgetData) throws QException { - List>> pvsData = new ArrayList<>(); - List pvsLabels = new ArrayList<>(); - List pvsNames = new ArrayList<>(); + List>> dataList = new ArrayList<>(); + List labelList = new ArrayList<>(); + List nameList = new ArrayList<>(); List missingRequiredSelections = new ArrayList<>(); for(WidgetDropdownData dropdownData : CollectionUtils.nonNullList(metaData.getDropdowns())) { - String possibleValueSourceName = dropdownData.getPossibleValueSourceName(); - QPossibleValueSource possibleValueSource = input.getInstance().getPossibleValueSource(possibleValueSourceName); - - ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - // this looks complicated, but is just look for a label in the dropdown data and if found use it, // - // otherwise look for label in PVS and if found use that, otherwise just use the PVS name // - ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - String pvsLabel = dropdownData.getLabel() != null ? dropdownData.getLabel() : (possibleValueSource.getLabel() != null ? possibleValueSource.getLabel() : possibleValueSourceName); - pvsLabels.add(pvsLabel); - pvsNames.add(possibleValueSourceName); - - SearchPossibleValueSourceInput pvsInput = new SearchPossibleValueSourceInput(); - pvsInput.setPossibleValueSourceName(possibleValueSourceName); - - if(dropdownData.getForeignKeyFieldName() != null) + if(WidgetDropdownType.DATE_PICKER.equals(dropdownData.getType())) { - //////////////////////////////////////// - // look for an id in the query params // - //////////////////////////////////////// - Integer id = null; - if(input.getQueryParams() != null && input.getQueryParams().containsKey("id") && StringUtils.hasContent(input.getQueryParams().get("id"))) + String name = dropdownData.getName(); + nameList.add(name); + labelList.add(dropdownData.getLabel()); + dataList.add(new ArrayList<>()); + + //////////////////////////////////////////////////////////////////////////////////////////////////////////// + // sure that something has been selected, and if not, display a message that a selection needs made // + //////////////////////////////////////////////////////////////////////////////////////////////////////////// + if(dropdownData.getIsRequired()) { - id = Integer.parseInt(input.getQueryParams().get("id")); - } - if(id != null) - { - pvsInput.setDefaultQueryFilter(new QQueryFilter().withCriteria( - new QFilterCriteria( - dropdownData.getForeignKeyFieldName(), - QCriteriaOperator.EQUALS, - id))); + if(!input.getQueryParams().containsKey(name) || !StringUtils.hasContent(input.getQueryParams().get(name))) + { + missingRequiredSelections.add(dropdownData.getLabel()); + } } } - - SearchPossibleValueSourceOutput output = new SearchPossibleValueSourceAction().execute(pvsInput); - - List> dropdownOptionList = new ArrayList<>(); - pvsData.add(dropdownOptionList); - - ////////////////////////////////////////// - // sort results, dedupe, and add to map // - ////////////////////////////////////////// - Set exists = new HashSet<>(); - output.getResults().removeIf(pvs -> !exists.add(pvs.getLabel())); - for(QPossibleValue possibleValue : output.getResults()) + else { - dropdownOptionList.add(MapBuilder.of( - "id", String.valueOf(possibleValue.getId()), - "label", possibleValue.getLabel() - )); - } - - //////////////////////////////////////////////////////////////////////////////////////////////////////////// - // because we know the dropdowns and what the field names will be when something is selected, we can make // - // sure that something has been selected, and if not, display a message that a selection needs made // - //////////////////////////////////////////////////////////////////////////////////////////////////////////// - if(dropdownData.getIsRequired()) - { - if(!input.getQueryParams().containsKey(possibleValueSourceName) || !StringUtils.hasContent(input.getQueryParams().get(possibleValueSourceName))) + String possibleValueSourceName = dropdownData.getPossibleValueSourceName(); + if(possibleValueSourceName != null) { - missingRequiredSelections.add(pvsLabel); + QPossibleValueSource possibleValueSource = input.getInstance().getPossibleValueSource(possibleValueSourceName); + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // this looks complicated, but is just look for a label in the dropdown data and if found use it, // + // otherwise look for label in PVS and if found use that, otherwise just use the PVS name // + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + String pvsLabel = dropdownData.getLabel() != null ? dropdownData.getLabel() : (possibleValueSource.getLabel() != null ? possibleValueSource.getLabel() : possibleValueSourceName); + labelList.add(pvsLabel); + nameList.add(possibleValueSourceName); + + SearchPossibleValueSourceInput pvsInput = new SearchPossibleValueSourceInput(); + pvsInput.setPossibleValueSourceName(possibleValueSourceName); + + if(dropdownData.getForeignKeyFieldName() != null) + { + //////////////////////////////////////// + // look for an id in the query params // + //////////////////////////////////////// + Integer id = null; + if(input.getQueryParams() != null && input.getQueryParams().containsKey("id") && StringUtils.hasContent(input.getQueryParams().get("id"))) + { + id = Integer.parseInt(input.getQueryParams().get("id")); + } + if(id != null) + { + pvsInput.setDefaultQueryFilter(new QQueryFilter().withCriteria( + new QFilterCriteria( + dropdownData.getForeignKeyFieldName(), + QCriteriaOperator.EQUALS, + id))); + } + } + + SearchPossibleValueSourceOutput output = new SearchPossibleValueSourceAction().execute(pvsInput); + + List> dropdownOptionList = new ArrayList<>(); + dataList.add(dropdownOptionList); + + ////////////////////////////////////////// + // sort results, dedupe, and add to map // + ////////////////////////////////////////// + Set exists = new HashSet<>(); + output.getResults().removeIf(pvs -> !exists.add(pvs.getLabel())); + for(QPossibleValue possibleValue : output.getResults()) + { + dropdownOptionList.add(MapBuilder.of( + "id", String.valueOf(possibleValue.getId()), + "label", possibleValue.getLabel() + )); + } + + //////////////////////////////////////////////////////////////////////////////////////////////////////////// + // because we know the dropdowns and what the field names will be when something is selected, we can make // + // sure that something has been selected, and if not, display a message that a selection needs made // + //////////////////////////////////////////////////////////////////////////////////////////////////////////// + if(dropdownData.getIsRequired()) + { + if(!input.getQueryParams().containsKey(possibleValueSourceName) || !StringUtils.hasContent(input.getQueryParams().get(possibleValueSourceName))) + { + missingRequiredSelections.add(pvsLabel); + } + } } } } - widgetData.setDropdownNameList(pvsNames); - widgetData.setDropdownLabelList(pvsLabels); - widgetData.setDropdownDataList(pvsData); + widgetData.setDropdownNameList(nameList); + widgetData.setDropdownLabelList(labelList); + widgetData.setDropdownDataList(dataList); //////////////////////////////////////////////////////////////////////////////// // if there are any missing required dropdowns, build up a message to display // diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/ReplaceAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/ReplaceAction.java index 841c6d6b..04177cdb 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/ReplaceAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/ReplaceAction.java @@ -47,6 +47,7 @@ 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.model.metadata.tables.UniqueKey; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import org.apache.commons.lang.BooleanUtils; /******************************************************************************* @@ -79,9 +80,11 @@ public class ReplaceAction extends AbstractQActionFunction, Serializable> existingKeys = UniqueKeyHelper.getExistingKeys(transaction, table, page, uniqueKey); + Map, Serializable> existingKeys = UniqueKeyHelper.getExistingKeys(transaction, table, page, uniqueKey, allowNullKeyValuesToEqual); + for(QRecord record : page) { - Optional> keyValues = UniqueKeyHelper.getKeyValues(table, uniqueKey, record); + Optional> keyValues = UniqueKeyHelper.getKeyValues(table, uniqueKey, record, allowNullKeyValuesToEqual); if(keyValues.isPresent()) { if(existingKeys.containsKey(keyValues.get())) diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/helpers/UniqueKeyHelper.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/helpers/UniqueKeyHelper.java index adab071e..7832344e 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/helpers/UniqueKeyHelper.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/helpers/UniqueKeyHelper.java @@ -54,7 +54,7 @@ public class UniqueKeyHelper /******************************************************************************* ** *******************************************************************************/ - public static Map, Serializable> getExistingKeys(QBackendTransaction transaction, QTableMetaData table, List recordList, UniqueKey uniqueKey) throws QException + public static Map, Serializable> getExistingKeys(QBackendTransaction transaction, QTableMetaData table, List recordList, UniqueKey uniqueKey, boolean allowNullKeyValuesToEqual) throws QException { List ukFieldNames = uniqueKey.getFieldNames(); Map, Serializable> existingRecords = new HashMap<>(); @@ -112,7 +112,7 @@ public class UniqueKeyHelper QueryOutput queryOutput = new QueryAction().execute(queryInput); for(QRecord record : queryOutput.getRecords()) { - Optional> keyValues = getKeyValues(table, uniqueKey, record); + Optional> keyValues = getKeyValues(table, uniqueKey, record, allowNullKeyValuesToEqual); if(keyValues.isPresent()) { existingRecords.put(keyValues.get(), record.getValue(table.getPrimaryKeyField())); @@ -128,7 +128,17 @@ public class UniqueKeyHelper /******************************************************************************* ** *******************************************************************************/ - public static Optional> getKeyValues(QTableMetaData table, UniqueKey uniqueKey, QRecord record) + public static Map, Serializable> getExistingKeys(QBackendTransaction transaction, QTableMetaData table, List recordList, UniqueKey uniqueKey) throws QException + { + return (getExistingKeys(transaction, table, recordList, uniqueKey, false)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static Optional> getKeyValues(QTableMetaData table, UniqueKey uniqueKey, QRecord record, boolean allowNullKeyValuesToEqual) { try { @@ -138,7 +148,19 @@ public class UniqueKeyHelper QFieldMetaData field = table.getField(fieldName); Serializable value = record.getValue(fieldName); Serializable typedValue = ValueUtils.getValueAsFieldType(field.getType(), value); - keyValues.add(typedValue == null ? new NullUniqueKeyValue() : typedValue); + + /////////////////////////////////////////////////////////////////////////////////// + // if null value, look at flag to determine if a null should be used (which will // + // allow keys to match), or a NullUniqueKeyValue, (which will never match) // + /////////////////////////////////////////////////////////////////////////////////// + if(typedValue == null) + { + keyValues.add(allowNullKeyValuesToEqual ? null : new NullUniqueKeyValue()); + } + else + { + keyValues.add(typedValue); + } } return (Optional.of(keyValues)); } @@ -150,6 +172,16 @@ public class UniqueKeyHelper + /******************************************************************************* + ** + *******************************************************************************/ + public static Optional> getKeyValues(QTableMetaData table, UniqueKey uniqueKey, QRecord record) + { + return (getKeyValues(table, uniqueKey, record, false)); + } + + + /******************************************************************************* ** To make a list of unique key values here behave like they do in an RDBMS ** (which is what we're trying to mimic - which is - 2 null values in a field diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/replace/ReplaceInput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/replace/ReplaceInput.java index 81944a6a..354a3bde 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/replace/ReplaceInput.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/replace/ReplaceInput.java @@ -39,7 +39,8 @@ public class ReplaceInput extends AbstractTableActionInput private UniqueKey key; private List records; private QQueryFilter filter; - private boolean performDeletes = true; + private boolean performDeletes = true; + private boolean allowNullKeyValuesToEqual = false; private boolean omitDmlAudit = false; @@ -239,4 +240,35 @@ public class ReplaceInput extends AbstractTableActionInput return (this); } + + + /******************************************************************************* + ** Getter for allowNullKeyValuesToEqual + *******************************************************************************/ + public boolean getAllowNullKeyValuesToEqual() + { + return (this.allowNullKeyValuesToEqual); + } + + + + /******************************************************************************* + ** Setter for allowNullKeyValuesToEqual + *******************************************************************************/ + public void setAllowNullKeyValuesToEqual(boolean allowNullKeyValuesToEqual) + { + this.allowNullKeyValuesToEqual = allowNullKeyValuesToEqual; + } + + + + /******************************************************************************* + ** Fluent setter for allowNullKeyValuesToEqual + *******************************************************************************/ + public ReplaceInput withAllowNullKeyValuesToEqual(boolean allowNullKeyValuesToEqual) + { + this.allowNullKeyValuesToEqual = allowNullKeyValuesToEqual; + return (this); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/AlertData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/AlertData.java new file mode 100644 index 00000000..28b60929 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/AlertData.java @@ -0,0 +1,139 @@ +/* + * 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.model.dashboard.widgets; + + +/******************************************************************************* + ** Model containing datastructure expected by frontend alert widget + ** + *******************************************************************************/ +public class AlertData extends QWidgetData +{ + public enum AlertType + { + ERROR, + SUCCESS, + WARNING + } + + + + private String html; + private AlertType alertType; + + + + /******************************************************************************* + ** + *******************************************************************************/ + public AlertData() + { + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public AlertData(AlertType alertType, String html) + { + setHtml(html); + setAlertType(alertType); + } + + + + /******************************************************************************* + ** Getter for type + ** + *******************************************************************************/ + public String getType() + { + return WidgetType.ALERT.getType(); + } + + + + /******************************************************************************* + ** Getter for html + ** + *******************************************************************************/ + public String getHtml() + { + return html; + } + + + + /******************************************************************************* + ** Setter for html + ** + *******************************************************************************/ + public void setHtml(String html) + { + this.html = html; + } + + + + /******************************************************************************* + ** Fluent setter for html + ** + *******************************************************************************/ + public AlertData withHtml(String html) + { + this.html = html; + return (this); + } + + + + /******************************************************************************* + ** Getter for alertType + *******************************************************************************/ + public AlertType getAlertType() + { + return (this.alertType); + } + + + + /******************************************************************************* + ** Setter for alertType + *******************************************************************************/ + public void setAlertType(AlertType alertType) + { + this.alertType = alertType; + } + + + + /******************************************************************************* + ** Fluent setter for alertType + *******************************************************************************/ + public AlertData withAlertType(AlertType alertType) + { + this.alertType = alertType; + return (this); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/WidgetType.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/WidgetType.java index a71ff27e..8b4061de 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/WidgetType.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/WidgetType.java @@ -27,6 +27,7 @@ package com.kingsrook.qqq.backend.core.model.dashboard.widgets; *******************************************************************************/ public enum WidgetType { + ALERT("alert"), BAR_CHART("barChart"), CHART("chart"), CHILD_RECORD_LIST("childRecordList"), diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/WidgetDropdownData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/WidgetDropdownData.java index d5446dea..37bd9d23 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/WidgetDropdownData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/WidgetDropdownData.java @@ -28,6 +28,7 @@ package com.kingsrook.qqq.backend.core.model.metadata.dashboard; *******************************************************************************/ public class WidgetDropdownData { + private String name; private String possibleValueSourceName; private String foreignKeyFieldName; private String label; @@ -44,6 +45,9 @@ public class WidgetDropdownData //////////////////////////////////////////////////////////////////////////////////////////////// private String labelForNullValue; + private WidgetDropdownType type = WidgetDropdownType.POSSIBLE_VALUE_SOURCE; + + /******************************************************************************* ** Getter for possibleValueSourceName @@ -366,4 +370,65 @@ public class WidgetDropdownData } + + /******************************************************************************* + ** Getter for type + *******************************************************************************/ + public WidgetDropdownType getType() + { + return (this.type); + } + + + + /******************************************************************************* + ** Setter for type + *******************************************************************************/ + public void setType(WidgetDropdownType type) + { + this.type = type; + } + + + + /******************************************************************************* + ** Fluent setter for type + *******************************************************************************/ + public WidgetDropdownData withType(WidgetDropdownType type) + { + this.type = type; + return (this); + } + + + + /******************************************************************************* + ** Getter for name + *******************************************************************************/ + public String getName() + { + return (this.name); + } + + + + /******************************************************************************* + ** Setter for name + *******************************************************************************/ + public void setName(String name) + { + this.name = name; + } + + + + /******************************************************************************* + ** Fluent setter for name + *******************************************************************************/ + public WidgetDropdownData withName(String name) + { + this.name = name; + return (this); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/WidgetDropdownType.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/WidgetDropdownType.java new file mode 100644 index 00000000..be47959f --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/WidgetDropdownType.java @@ -0,0 +1,33 @@ +/* + * 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.model.metadata.dashboard; + + +/******************************************************************************* + ** Possible types for widget dropdowns + ** + *******************************************************************************/ +public enum WidgetDropdownType +{ + POSSIBLE_VALUE_SOURCE, + DATE_PICKER +} diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/ReplaceActionTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/ReplaceActionTest.java index 4ce0e8ee..da57c703 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/ReplaceActionTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/ReplaceActionTest.java @@ -43,7 +43,7 @@ import static org.junit.jupiter.api.Assertions.assertNull; /******************************************************************************* - ** Unit test for ReplaceAction + ** Unit test for ReplaceAction *******************************************************************************/ class ReplaceActionTest extends BaseTest { @@ -157,6 +157,134 @@ class ReplaceActionTest extends BaseTest + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testTwoKeysWithNullsNotMatchingAllowingDelete() throws QException + { + String tableName = TestUtils.TABLE_NAME_TWO_KEYS; + + //////////////////////////////// + // start with these 2 records // + //////////////////////////////// + new InsertAction().execute(new InsertInput(tableName).withRecords(List.of( + new QRecord().withValue("key1", 1).withValue("key2", 2), + new QRecord().withValue("key1", 3) + ))); + + //////////////////////////////////////////////////// + // now do a replace action that just updates them // + //////////////////////////////////////////////////// + List newThings = List.of( + new QRecord().withValue("key1", 1).withValue("key2", 2), + new QRecord().withValue("key1", 3) + ); + + ////////////////////////////// + // replace allowing deletes // + ////////////////////////////// + ReplaceInput replaceInput = new ReplaceInput(); + replaceInput.setTableName(tableName); + replaceInput.setKey(new UniqueKey("key1", "key2")); + replaceInput.setOmitDmlAudit(true); + replaceInput.setRecords(newThings); + replaceInput.setFilter(null); + ReplaceOutput replaceOutput = new ReplaceAction().execute(replaceInput); + + assertEquals(1, replaceOutput.getInsertOutput().getRecords().size()); + assertEquals(1, replaceOutput.getUpdateOutput().getRecords().size()); + assertEquals(1, replaceOutput.getDeleteOutput().getDeletedRecordCount()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testTwoKeysWithNullsNotMatchingNotAllowingDelete() throws QException + { + String tableName = TestUtils.TABLE_NAME_TWO_KEYS; + + //////////////////////////////// + // start with these 2 records // + //////////////////////////////// + new InsertAction().execute(new InsertInput(tableName).withRecords(List.of( + new QRecord().withValue("key1", 1).withValue("key2", 2), + new QRecord().withValue("key1", 3) + ))); + + //////////////////////////////////////////////////// + // now do a replace action that just updates them // + //////////////////////////////////////////////////// + List newThings = List.of( + new QRecord().withValue("key1", 1).withValue("key2", 2), + new QRecord().withValue("key1", 3) + ); + + ///////////////////////////////// + // replace disallowing deletes // + ///////////////////////////////// + ReplaceInput replaceInput = new ReplaceInput(); + replaceInput.setTableName(tableName); + replaceInput.setKey(new UniqueKey("key1", "key2")); + replaceInput.setOmitDmlAudit(true); + replaceInput.setRecords(newThings); + replaceInput.setFilter(null); + replaceInput.setPerformDeletes(false); + ReplaceOutput replaceOutput = new ReplaceAction().execute(replaceInput); + + assertEquals(1, replaceOutput.getInsertOutput().getRecords().size()); + assertEquals(1, replaceOutput.getUpdateOutput().getRecords().size()); + assertNull(replaceOutput.getDeleteOutput()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testTwoKeysWithNullMatching() throws QException + { + String tableName = TestUtils.TABLE_NAME_TWO_KEYS; + + //////////////////////////////// + // start with these 2 records // + //////////////////////////////// + new InsertAction().execute(new InsertInput(tableName).withRecords(List.of( + new QRecord().withValue("key1", 1).withValue("key2", 2), + new QRecord().withValue("key1", 3) + ))); + + //////////////////////////////////////////////////// + // now do a replace action that just updates them // + //////////////////////////////////////////////////// + List newThings = List.of( + new QRecord().withValue("key1", 1).withValue("key2", 2), + new QRecord().withValue("key1", 3) + ); + + /////////////////////////////////////////////// + // replace treating null key values as equal // + /////////////////////////////////////////////// + ReplaceInput replaceInput = new ReplaceInput(); + replaceInput.setTableName(tableName); + replaceInput.setKey(new UniqueKey("key1", "key2")); + replaceInput.setOmitDmlAudit(true); + replaceInput.setRecords(newThings); + replaceInput.setFilter(null); + replaceInput.setAllowNullKeyValuesToEqual(true); + ReplaceOutput replaceOutput = new ReplaceAction().execute(replaceInput); + + assertEquals(0, replaceOutput.getInsertOutput().getRecords().size()); + assertEquals(2, replaceOutput.getUpdateOutput().getRecords().size()); + assertEquals(0, replaceOutput.getDeleteOutput().getDeletedRecordCount()); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -297,4 +425,4 @@ class ReplaceActionTest extends BaseTest return new GetAction().executeForRecord(new GetInput(TestUtils.TABLE_NAME_PERSON_MEMORY).withUniqueKey(Map.of("firstName", firstName, "lastName", lastName))).getValueInteger("noOfShoes"); } -} \ 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 0086c351..000afa15 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 @@ -138,6 +138,7 @@ public class TestUtils public static final String APP_NAME_PEOPLE = "peopleApp"; public static final String APP_NAME_MISCELLANEOUS = "miscellaneous"; + public static final String TABLE_NAME_TWO_KEYS = "twoKeys"; public static final String TABLE_NAME_PERSON = "person"; public static final String TABLE_NAME_SHAPE = "shape"; public static final String TABLE_NAME_SHAPE_CACHE = "shapeCache"; @@ -196,6 +197,7 @@ public class TestUtils qInstance.addBackend(defineMemoryBackend()); qInstance.addTable(defineTablePerson()); + qInstance.addTable(defineTableTwoKeys()); qInstance.addTable(definePersonFileTable()); qInstance.addTable(definePersonMemoryTable()); qInstance.addTable(definePersonMemoryCacheTable()); @@ -545,6 +547,24 @@ public class TestUtils + /******************************************************************************* + ** Define the 'two key' table used in standard tests. + *******************************************************************************/ + public static QTableMetaData defineTableTwoKeys() + { + return new QTableMetaData() + .withName(TABLE_NAME_TWO_KEYS) + .withLabel("Two Keys") + .withBackendName(MEMORY_BACKEND_NAME) + .withPrimaryKeyField("id") + .withField(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false)) + .withUniqueKey(new UniqueKey("key1", "key2")) + .withField(new QFieldMetaData("key1", QFieldType.INTEGER)) + .withField(new QFieldMetaData("key2", QFieldType.INTEGER)); + } + + + /******************************************************************************* ** Define the 'person' table used in standard tests. *******************************************************************************/ @@ -791,6 +811,26 @@ public class TestUtils + /******************************************************************************* + ** Define a table with unique key where one is nullable + *******************************************************************************/ + public static QTableMetaData defineTwoKeyTable() + { + return (new QTableMetaData() + .withName(TABLE_NAME_BASEPULL) + .withLabel("Basepull Test") + .withPrimaryKeyField("id") + .withBackendName(MEMORY_BACKEND_NAME) + .withFields(TestUtils.defineTablePerson().getFields())) + .withField(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false)) + .withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME).withBackendName("create_date").withIsEditable(false)) + .withField(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME).withBackendName("modify_date").withIsEditable(false)) + .withField(new QFieldMetaData(BASEPULL_KEY_FIELD_NAME, QFieldType.STRING).withBackendName("process_name").withIsRequired(true)) + .withField(new QFieldMetaData(BASEPULL_LAST_RUN_TIME_FIELD_NAME, QFieldType.DATE_TIME).withBackendName("last_run_time").withIsRequired(true)); + } + + + /******************************************************************************* ** Define a basepullTable *******************************************************************************/