diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QCustomPossibleValueProvider.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QCustomPossibleValueProvider.java index 09e786c5..09bbd74e 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QCustomPossibleValueProvider.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QCustomPossibleValueProvider.java @@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.core.actions.values; import java.io.Serializable; import java.util.ArrayList; +import java.util.Comparator; import java.util.List; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.actions.values.SearchPossibleValueSourceInput; @@ -74,6 +75,29 @@ public interface QCustomPossibleValueProvider } + /*************************************************************************** + * + ***************************************************************************/ + default List> _defaultSearch(SearchPossibleValueSourceInput input, List> possibleValues) + { + SearchPossibleValueSourceAction.PreparedSearchPossibleValueSourceInput preparedInput = SearchPossibleValueSourceAction.prepareSearchPossibleValueSourceInput(input); + + List> rs = new ArrayList<>(); + + for(QPossibleValue possibleValue : possibleValues) + { + if(possibleValue != null && SearchPossibleValueSourceAction.doesPossibleValueMatchSearchInput(possibleValue, preparedInput)) + { + rs.add(possibleValue); + } + } + + rs.sort(Comparator.nullsLast(Comparator.comparing((QPossibleValue pv) -> pv.getLabel()))); + + return (rs); + } + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/SearchPossibleValueSourceAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/SearchPossibleValueSourceAction.java index 97bcae1c..2063f39f 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/SearchPossibleValueSourceAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/SearchPossibleValueSourceAction.java @@ -26,6 +26,7 @@ import java.io.Serializable; import java.time.Instant; import java.time.LocalDate; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; @@ -51,7 +52,6 @@ import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleVal import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource; import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; -import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.core.utils.ValueUtils; import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; @@ -108,60 +108,54 @@ public class SearchPossibleValueSourceAction + /*************************************************************************** + ** record to store "computed" values as part of a possible-value search - + ** e.g., ids type-convered, and lower-cased labels. + ***************************************************************************/ + public record PreparedSearchPossibleValueSourceInput(Collection inputIdsAsCorrectType, Collection lowerCaseLabels, String searchTerm) {} + + + + /*************************************************************************** + ** + ***************************************************************************/ + public static PreparedSearchPossibleValueSourceInput prepareSearchPossibleValueSourceInput(SearchPossibleValueSourceInput input) + { + QPossibleValueSource possibleValueSource = QContext.getQInstance().getPossibleValueSource(input.getPossibleValueSourceName()); + List inputIdsAsCorrectType = convertInputIdsToPossibleValueSourceIdType(possibleValueSource, input.getIdList()); + + Set lowerCaseLabels = null; + if(input.getLabelList() != null) + { + lowerCaseLabels = input.getLabelList().stream() + .filter(Objects::nonNull) + .map(l -> l.toLowerCase()) + .collect(Collectors.toSet()); + } + + return (new PreparedSearchPossibleValueSourceInput(inputIdsAsCorrectType, lowerCaseLabels, input.getSearchTerm())); + } + + + /******************************************************************************* ** *******************************************************************************/ private SearchPossibleValueSourceOutput searchPossibleValueEnum(SearchPossibleValueSourceInput input, QPossibleValueSource possibleValueSource) { + PreparedSearchPossibleValueSourceInput preparedSearchPossibleValueSourceInput = prepareSearchPossibleValueSourceInput(input); + SearchPossibleValueSourceOutput output = new SearchPossibleValueSourceOutput(); List matchingIds = new ArrayList<>(); - List inputIdsAsCorrectType = convertInputIdsToEnumIdType(possibleValueSource, input.getIdList()); - Set labels = null; - for(QPossibleValue possibleValue : possibleValueSource.getEnumValues()) { - boolean match = false; - - if(input.getIdList() != null) - { - if(inputIdsAsCorrectType.contains(possibleValue.getId())) - { - match = true; - } - } - else if(input.getLabelList() != null) - { - if(labels == null) - { - labels = input.getLabelList().stream().filter(Objects::nonNull).map(l -> l.toLowerCase()).collect(Collectors.toSet()); - } - - if(labels.contains(possibleValue.getLabel().toLowerCase())) - { - match = true; - } - } - else - { - if(StringUtils.hasContent(input.getSearchTerm())) - { - match = (Objects.equals(ValueUtils.getValueAsString(possibleValue.getId()).toLowerCase(), input.getSearchTerm().toLowerCase()) - || possibleValue.getLabel().toLowerCase().startsWith(input.getSearchTerm().toLowerCase())); - } - else - { - match = true; - } - } + boolean match = doesPossibleValueMatchSearchInput(possibleValue, preparedSearchPossibleValueSourceInput); if(match) { - matchingIds.add((Serializable) possibleValue.getId()); + matchingIds.add(possibleValue.getId()); } - - // todo - skip & limit? - // todo - default filter } List> qPossibleValues = possibleValueTranslator.buildTranslatedPossibleValueList(possibleValueSource, matchingIds); @@ -172,42 +166,84 @@ public class SearchPossibleValueSourceAction + /*************************************************************************** + ** + ***************************************************************************/ + public static boolean doesPossibleValueMatchSearchInput(QPossibleValue possibleValue, PreparedSearchPossibleValueSourceInput input) + { + boolean match = false; + + if(input.inputIdsAsCorrectType() != null) + { + if(input.inputIdsAsCorrectType().contains(possibleValue.getId())) + { + match = true; + } + } + else if(input.lowerCaseLabels() != null) + { + if(input.lowerCaseLabels().contains(possibleValue.getLabel().toLowerCase())) + { + match = true; + } + } + else + { + if(StringUtils.hasContent(input.searchTerm())) + { + match = (Objects.equals(ValueUtils.getValueAsString(possibleValue.getId()).toLowerCase(), input.searchTerm().toLowerCase()) + || possibleValue.getLabel().toLowerCase().startsWith(input.searchTerm().toLowerCase())); + } + else + { + match = true; + } + } + return match; + } + + + /******************************************************************************* ** The input list of ids might come through as a type that isn't the same as ** the type of the ids in the enum (e.g., strings from a frontend, integers - ** in an enum). So, this method looks at the first id in the enum, and then - ** maps all the inputIds to be of the same type. + ** in an enum). So, this method type-converts them. *******************************************************************************/ - private List convertInputIdsToEnumIdType(QPossibleValueSource possibleValueSource, List inputIdList) + private static List convertInputIdsToPossibleValueSourceIdType(QPossibleValueSource possibleValueSource, List inputIdList) { List rs = new ArrayList<>(); - if(CollectionUtils.nullSafeIsEmpty(inputIdList)) + + if(inputIdList == null) + { + return (null); + } + else if(inputIdList.isEmpty()) { return (rs); } - Object anIdFromTheEnum = possibleValueSource.getEnumValues().get(0).getId(); + QFieldType type = possibleValueSource.getIdType(); for(Serializable inputId : inputIdList) { Object properlyTypedId = null; try { - if(anIdFromTheEnum instanceof Integer) + if(type.equals(QFieldType.INTEGER)) { properlyTypedId = ValueUtils.getValueAsInteger(inputId); } - else if(anIdFromTheEnum instanceof String) + else if(type.isStringLike()) { properlyTypedId = ValueUtils.getValueAsString(inputId); } - else if(anIdFromTheEnum instanceof Boolean) + else if(type.equals(QFieldType.BOOLEAN)) { properlyTypedId = ValueUtils.getValueAsBoolean(inputId); } else { - LOG.warn("Unexpected type [" + anIdFromTheEnum.getClass().getSimpleName() + "] for ids in enum: " + possibleValueSource.getName()); + LOG.warn("Unexpected type [" + type + "] for ids in enum: " + possibleValueSource.getName()); } } catch(Exception e) @@ -215,7 +251,7 @@ public class SearchPossibleValueSourceAction LOG.debug("Error converting possible value id to expected id type", e, logPair("value", inputId)); } - if (properlyTypedId != null) + if(properlyTypedId != null) { rs.add(properlyTypedId); } @@ -397,7 +433,7 @@ public class SearchPossibleValueSourceAction } catch(Exception e) { - String message = "Error sending searching custom possible value source [" + input.getPossibleValueSourceName() + "]"; + String message = "Error searching custom possible value source [" + input.getPossibleValueSourceName() + "]"; LOG.warn(message, e); throw (new QException(message)); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/TablesCustomPossibleValueProvider.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/TablesCustomPossibleValueProvider.java new file mode 100644 index 00000000..93e97d83 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/TablesCustomPossibleValueProvider.java @@ -0,0 +1,88 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2025. 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.tables; + + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import com.kingsrook.qqq.backend.core.actions.permissions.PermissionCheckResult; +import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper; +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.tables.query.QueryInput; +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.utils.ValueUtils; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class TablesCustomPossibleValueProvider implements QCustomPossibleValueProvider +{ + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public QPossibleValue getPossibleValue(Serializable idValue) + { + QTableMetaData table = QContext.getQInstance().getTable(ValueUtils.getValueAsString(idValue)); + if(table != null && !table.getIsHidden()) + { + PermissionCheckResult permissionCheckResult = PermissionsHelper.getPermissionCheckResult(new QueryInput(table.getName()), table); + if(PermissionCheckResult.ALLOW.equals(permissionCheckResult)) + { + return (new QPossibleValue<>(table.getName(), table.getLabel())); + } + } + + return null; + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public List> search(SearchPossibleValueSourceInput input) throws QException + { + ///////////////////////////////////////////////////////////////////////////////////// + // build all of the possible values (note, will be filtered by user's permissions) // + ///////////////////////////////////////////////////////////////////////////////////// + List> allPossibleValues = new ArrayList<>(); + for(QTableMetaData table : QContext.getQInstance().getTables().values()) + { + QPossibleValue possibleValue = getPossibleValue(table.getName()); + if(possibleValue != null) + { + allPossibleValues.add(possibleValue); + } + } + + return _defaultSearch(input, allPossibleValues); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/TablesPossibleValueSourceMetaDataProvider.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/TablesPossibleValueSourceMetaDataProvider.java index 27cce6df..75087ae7 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/TablesPossibleValueSourceMetaDataProvider.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/TablesPossibleValueSourceMetaDataProvider.java @@ -22,17 +22,11 @@ package com.kingsrook.qqq.backend.core.model.metadata.tables; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.List; -import com.kingsrook.qqq.backend.core.instances.QInstanceEnricher; 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.possiblevalues.PVSValueFormatAndFields; -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.model.metadata.possiblevalues.QPossibleValueSourceType; -import com.kingsrook.qqq.backend.core.utils.StringUtils; -import org.apache.commons.lang.BooleanUtils; /******************************************************************************* @@ -51,22 +45,10 @@ public class TablesPossibleValueSourceMetaDataProvider { QPossibleValueSource possibleValueSource = new QPossibleValueSource() .withName(NAME) - .withType(QPossibleValueSourceType.ENUM) + .withType(QPossibleValueSourceType.CUSTOM) + .withCustomCodeReference(new QCodeReference(TablesCustomPossibleValueProvider.class)) .withValueFormatAndFields(PVSValueFormatAndFields.LABEL_ONLY); - List> enumValues = new ArrayList<>(); - for(QTableMetaData table : qInstance.getTables().values()) - { - if(BooleanUtils.isNotTrue(table.getIsHidden())) - { - String label = StringUtils.hasContent(table.getLabel()) ? table.getLabel() : QInstanceEnricher.nameToLabel(table.getName()); - enumValues.add(new QPossibleValue<>(table.getName(), label)); - } - } - - enumValues.sort(Comparator.comparing(QPossibleValue::getLabel)); - - possibleValueSource.withEnumValues(enumValues); return (possibleValueSource); } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/metadata/tables/TablesCustomPossibleValueProviderTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/metadata/tables/TablesCustomPossibleValueProviderTest.java new file mode 100644 index 00000000..4bdbbc06 --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/metadata/tables/TablesCustomPossibleValueProviderTest.java @@ -0,0 +1,157 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2025. 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.tables; + + +import java.util.List; +import com.kingsrook.qqq.backend.core.BaseTest; +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.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; +import com.kingsrook.qqq.backend.core.model.metadata.permissions.PermissionLevel; +import com.kingsrook.qqq.backend.core.model.metadata.permissions.QPermissionRules; +import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValue; +import com.kingsrook.qqq.backend.core.utils.TestUtils; +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 TablesCustomPossibleValueProvider + *******************************************************************************/ +class TablesCustomPossibleValueProviderTest extends BaseTest +{ + /******************************************************************************* + ** + *******************************************************************************/ + @BeforeEach + void beforeEach() + { + QInstance qInstance = TestUtils.defineInstance(); + + qInstance.addTable(new QTableMetaData() + .withName("hidden") + .withIsHidden(true) + .withBackendName(TestUtils.MEMORY_BACKEND_NAME) + .withPrimaryKeyField("id") + .withField(new QFieldMetaData("id", QFieldType.INTEGER))); + + qInstance.addTable(new QTableMetaData() + .withName("restricted") + .withPermissionRules(new QPermissionRules().withLevel(PermissionLevel.HAS_ACCESS_PERMISSION)) + .withBackendName(TestUtils.MEMORY_BACKEND_NAME) + .withPrimaryKeyField("id") + .withField(new QFieldMetaData("id", QFieldType.INTEGER))); + + qInstance.addPossibleValueSource(TablesPossibleValueSourceMetaDataProvider.defineTablesPossibleValueSource(qInstance)); + + QContext.init(qInstance, newSession()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testGetPossibleValue() + { + TablesCustomPossibleValueProvider provider = new TablesCustomPossibleValueProvider(); + + QPossibleValue possibleValue = provider.getPossibleValue(TestUtils.TABLE_NAME_PERSON); + assertEquals(TestUtils.TABLE_NAME_PERSON, possibleValue.getId()); + assertEquals("Person", possibleValue.getLabel()); + + assertNull(provider.getPossibleValue("no-such-table")); + assertNull(provider.getPossibleValue("hidden")); + assertNull(provider.getPossibleValue("restricted")); + + QContext.getQSession().withPermission("restricted.hasAccess"); + assertNotNull(provider.getPossibleValue("restricted")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testSearchPossibleValue() throws QException + { + TablesCustomPossibleValueProvider provider = new TablesCustomPossibleValueProvider(); + + List> list = provider.search(new SearchPossibleValueSourceInput() + .withPossibleValueSourceName(TablesPossibleValueSourceMetaDataProvider.NAME)); + assertThat(list).anyMatch(p -> p.getId().equals(TestUtils.TABLE_NAME_PERSON)); + assertThat(list).noneMatch(p -> p.getId().equals("no-such-table")); + assertThat(list).noneMatch(p -> p.getId().equals("hidden")); + assertThat(list).noneMatch(p -> p.getId().equals("restricted")); + assertNull(provider.getPossibleValue("restricted")); + + list = provider.search(new SearchPossibleValueSourceInput() + .withPossibleValueSourceName(TablesPossibleValueSourceMetaDataProvider.NAME) + .withIdList(List.of(TestUtils.TABLE_NAME_PERSON, TestUtils.TABLE_NAME_SHAPE, "hidden"))); + assertEquals(2, list.size()); + assertThat(list).anyMatch(p -> p.getId().equals(TestUtils.TABLE_NAME_PERSON)); + assertThat(list).anyMatch(p -> p.getId().equals(TestUtils.TABLE_NAME_SHAPE)); + assertThat(list).noneMatch(p -> p.getId().equals("hidden")); + + list = provider.search(new SearchPossibleValueSourceInput() + .withPossibleValueSourceName(TablesPossibleValueSourceMetaDataProvider.NAME) + .withLabelList(List.of("Person", "Shape", "Restricted"))); + assertEquals(2, list.size()); + assertThat(list).anyMatch(p -> p.getId().equals(TestUtils.TABLE_NAME_PERSON)); + assertThat(list).anyMatch(p -> p.getId().equals(TestUtils.TABLE_NAME_SHAPE)); + assertThat(list).noneMatch(p -> p.getId().equals("restricted")); + + list = provider.search(new SearchPossibleValueSourceInput() + .withPossibleValueSourceName(TablesPossibleValueSourceMetaDataProvider.NAME) + .withSearchTerm("restricted")); + assertEquals(0, list.size()); + + ///////////////////////////////////////// + // add permission for restricted table // + ///////////////////////////////////////// + QContext.getQSession().withPermission("restricted.hasAccess"); + list = provider.search(new SearchPossibleValueSourceInput() + .withPossibleValueSourceName(TablesPossibleValueSourceMetaDataProvider.NAME) + .withSearchTerm("restricted")); + assertEquals(1, list.size()); + + list = provider.search(new SearchPossibleValueSourceInput() + .withPossibleValueSourceName(TablesPossibleValueSourceMetaDataProvider.NAME) + .withLabelList(List.of("Person", "Shape", "Restricted"))); + assertEquals(3, list.size()); + assertThat(list).anyMatch(p -> p.getId().equals(TestUtils.TABLE_NAME_PERSON)); + assertThat(list).anyMatch(p -> p.getId().equals(TestUtils.TABLE_NAME_SHAPE)); + assertThat(list).anyMatch(p -> p.getId().equals("restricted")); + + } + +} \ No newline at end of file