From c43a8998ecaffbdaaf8410d6e7fed45ce7435bdb Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Mon, 10 Oct 2022 16:42:49 -0500 Subject: [PATCH] Updating to support possible value searching --- .../values/QPossibleValueTranslator.java | 58 +++- .../SearchPossibleValueSourceAction.java | 244 ++++++++++++++++ .../core/instances/QInstanceEnricher.java | 51 ++++ .../core/instances/QInstanceValidator.java | 34 ++- .../SearchPossibleValueSourceInput.java | 268 ++++++++++++++++++ .../SearchPossibleValueSourceOutput.java | 91 ++++++ .../possiblevalues/QPossibleValueSource.java | 128 ++++++++- .../memory/MemoryRecordStore.java | 12 +- .../qqq/backend/core/utils/ValueUtils.java | 29 +- .../SearchPossibleValueSourceActionTest.java | 243 ++++++++++++++++ .../instances/QInstanceValidatorTest.java | 18 +- .../qqq/backend/core/utils/TestUtils.java | 4 +- .../javalin/QJavalinImplementation.java | 64 +++++ .../javalin/QJavalinImplementationTest.java | 53 ++++ .../qqq/backend/javalin/TestUtils.java | 37 ++- .../test/resources/prime-test-database.sql | 3 +- 16 files changed, 1312 insertions(+), 25 deletions(-) create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/SearchPossibleValueSourceAction.java create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/values/SearchPossibleValueSourceInput.java create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/values/SearchPossibleValueSourceOutput.java create mode 100644 qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/values/SearchPossibleValueSourceActionTest.java diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QPossibleValueTranslator.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QPossibleValueTranslator.java index 624ba712..3a22f754 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QPossibleValueTranslator.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QPossibleValueTranslator.java @@ -111,7 +111,35 @@ public class QPossibleValueTranslator /******************************************************************************* - ** + ** Translate a list of ids to a list of possible values (e.g., w/ rendered values) + *******************************************************************************/ + public List> buildTranslatedPossibleValueList(QPossibleValueSource possibleValueSource, List ids) + { + if(ids == null) + { + return (null); + } + + if(ids.isEmpty()) + { + return (new ArrayList<>()); + } + + List> rs = new ArrayList<>(); + primePvsCache(possibleValueSource.getTableName(), List.of(possibleValueSource), ids); + for(Serializable id : ids) + { + String translated = translatePossibleValue(possibleValueSource, id); + rs.add(new QPossibleValue<>(id, translated)); + } + + return (rs); + } + + + + /******************************************************************************* + ** For a given field and (raw/id) value, get the translated (string) value. *******************************************************************************/ String translatePossibleValue(QFieldMetaData field, Serializable value) { @@ -122,6 +150,16 @@ public class QPossibleValueTranslator return (null); } + return translatePossibleValue(possibleValueSource, value); + } + + + + /******************************************************************************* + ** For a given PossibleValueSource and (raw/id) value, get the translated (string) value. + *******************************************************************************/ + String translatePossibleValue(QPossibleValueSource possibleValueSource, Serializable value) + { String resultValue = null; if(possibleValueSource.getType().equals(QPossibleValueSourceType.ENUM)) { @@ -129,15 +167,15 @@ public class QPossibleValueTranslator } else if(possibleValueSource.getType().equals(QPossibleValueSourceType.TABLE)) { - resultValue = translatePossibleValueTable(field, value, possibleValueSource); + resultValue = translatePossibleValueTable(value, possibleValueSource); } else if(possibleValueSource.getType().equals(QPossibleValueSourceType.CUSTOM)) { - resultValue = translatePossibleValueCustom(field, value, possibleValueSource); + resultValue = translatePossibleValueCustom(value, possibleValueSource); } else { - LOG.error("Unrecognized possibleValueSourceType [" + possibleValueSource.getType() + "] in PVS named [" + possibleValueSource.getName() + "] on field [" + field.getName() + "]"); + LOG.error("Unrecognized possibleValueSourceType [" + possibleValueSource.getType() + "] in PVS named [" + possibleValueSource.getName() + "]"); } if(resultValue == null) @@ -151,7 +189,7 @@ public class QPossibleValueTranslator /******************************************************************************* - ** + ** do translation for an enum-type PVS *******************************************************************************/ private String translatePossibleValueEnum(Serializable value, QPossibleValueSource possibleValueSource) { @@ -169,9 +207,9 @@ public class QPossibleValueTranslator /******************************************************************************* - ** + ** do translation for a table-type PVS *******************************************************************************/ - private String translatePossibleValueTable(QFieldMetaData field, Serializable value, QPossibleValueSource possibleValueSource) + String translatePossibleValueTable(Serializable value, QPossibleValueSource possibleValueSource) { ///////////////////////////////// // null input gets null output // @@ -197,9 +235,9 @@ public class QPossibleValueTranslator /******************************************************************************* - ** + ** do translation for a custom-type PVS *******************************************************************************/ - private String translatePossibleValueCustom(QFieldMetaData field, Serializable value, QPossibleValueSource possibleValueSource) + private String translatePossibleValueCustom(Serializable value, QPossibleValueSource possibleValueSource) { try { @@ -208,7 +246,7 @@ public class QPossibleValueTranslator } catch(Exception e) { - LOG.warn("Error sending [" + value + "] for field [" + field + "] through custom code for PVS [" + field.getPossibleValueSourceName() + "]", e); + LOG.warn("Error sending [" + value + "] for through custom code for PVS [" + possibleValueSource.getName() + "]", e); } return (null); 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 new file mode 100644 index 00000000..f6490000 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/SearchPossibleValueSourceAction.java @@ -0,0 +1,244 @@ +/* + * 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.actions.values; + + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput; +import com.kingsrook.qqq.backend.core.model.actions.values.SearchPossibleValueSourceInput; +import com.kingsrook.qqq.backend.core.model.actions.values.SearchPossibleValueSourceOutput; +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.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.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.utils.StringUtils; +import com.kingsrook.qqq.backend.core.utils.ValueUtils; +import org.apache.commons.lang.NotImplementedException; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + + +/******************************************************************************* + ** Class responsible for looking up possible-values for fields/records and + ** make them into display values. + *******************************************************************************/ +public class SearchPossibleValueSourceAction +{ + private static final Logger LOG = LogManager.getLogger(SearchPossibleValueSourceAction.class); + + private QPossibleValueTranslator possibleValueTranslator; + + + + /******************************************************************************* + ** + *******************************************************************************/ + public SearchPossibleValueSourceOutput execute(SearchPossibleValueSourceInput input) throws QException + { + QInstance qInstance = input.getInstance(); + QPossibleValueSource possibleValueSource = qInstance.getPossibleValueSource(input.getPossibleValueSourceName()); + if(possibleValueSource == null) + { + throw new QException("Missing possible value source named [" + input.getPossibleValueSourceName() + "]"); + } + + possibleValueTranslator = new QPossibleValueTranslator(input.getInstance(), input.getSession()); + SearchPossibleValueSourceOutput output = null; + if(possibleValueSource.getType().equals(QPossibleValueSourceType.ENUM)) + { + output = searchPossibleValueEnum(input, possibleValueSource); + } + else if(possibleValueSource.getType().equals(QPossibleValueSourceType.TABLE)) + { + output = searchPossibleValueTable(input, possibleValueSource); + } + else if(possibleValueSource.getType().equals(QPossibleValueSourceType.CUSTOM)) + { + output = searchPossibleValueCustom(input, possibleValueSource); + } + else + { + LOG.error("Unrecognized possibleValueSourceType [" + possibleValueSource.getType() + "] in PVS named [" + possibleValueSource.getName() + "]"); + } + + return (output); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private SearchPossibleValueSourceOutput searchPossibleValueEnum(SearchPossibleValueSourceInput input, QPossibleValueSource possibleValueSource) + { + SearchPossibleValueSourceOutput output = new SearchPossibleValueSourceOutput(); + List matchingIds = new ArrayList<>(); + + for(QPossibleValue possibleValue : possibleValueSource.getEnumValues()) + { + boolean match = false; + + if(input.getIdList() != null) + { + if(input.getIdList().contains(possibleValue.getId())) + { + 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; + } + } + + if(match) + { + matchingIds.add((Serializable) possibleValue.getId()); + } + + // todo - skip & limit? + // todo - default filter + } + + List> qPossibleValues = possibleValueTranslator.buildTranslatedPossibleValueList(possibleValueSource, matchingIds); + output.setResults(qPossibleValues); + + return (output); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private SearchPossibleValueSourceOutput searchPossibleValueTable(SearchPossibleValueSourceInput input, QPossibleValueSource possibleValueSource) throws QException + { + SearchPossibleValueSourceOutput output = new SearchPossibleValueSourceOutput(); + + QueryInput queryInput = new QueryInput(input.getInstance()); + queryInput.setSession(input.getSession()); + queryInput.setTableName(possibleValueSource.getTableName()); + + QTableMetaData table = input.getInstance().getTable(possibleValueSource.getTableName()); + + QQueryFilter queryFilter = new QQueryFilter(); + queryFilter.setBooleanOperator(QQueryFilter.BooleanOperator.OR); + queryInput.setFilter(queryFilter); + + if(input.getIdList() != null) + { + queryFilter.addCriteria(new QFilterCriteria(table.getPrimaryKeyField(), QCriteriaOperator.IN, input.getIdList())); + } + else + { + if(StringUtils.hasContent(input.getSearchTerm())) + { + for(String valueField : possibleValueSource.getSearchFields()) + { + QFieldMetaData field = table.getField(valueField); + if(field.getType().equals(QFieldType.STRING)) + { + queryFilter.addCriteria(new QFilterCriteria(valueField, QCriteriaOperator.STARTS_WITH, List.of(input.getSearchTerm()))); + } + else if(field.getType().equals(QFieldType.DATE) || field.getType().equals(QFieldType.DATE_TIME)) + { + LOG.debug("Not querying PVS [" + possibleValueSource.getName() + "] on date field [" + field.getName() + "]"); + // todo - what? queryFilter.addCriteria(new QFilterCriteria(valueField, QCriteriaOperator.STARTS_WITH, List.of(input.getSearchTerm()))); + } + else + { + try + { + Integer valueAsInteger = ValueUtils.getValueAsInteger(input.getSearchTerm()); + if(valueAsInteger != null) + { + queryFilter.addCriteria(new QFilterCriteria(valueField, QCriteriaOperator.EQUALS, List.of(valueAsInteger))); + } + } + catch(Exception e) + { + //////////////////////////////////////////////////////// + // write a FALSE criteria if the value isn't a number // + //////////////////////////////////////////////////////// + queryFilter.addCriteria(new QFilterCriteria(valueField, QCriteriaOperator.IN, List.of())); + } + } + } + } + } + + queryFilter.setOrderBys(possibleValueSource.getOrderByFields()); + + // todo - default filter + + // todo - skip & limit as params + queryInput.setLimit(250); + + QueryOutput queryOutput = new QueryAction().execute(queryInput); + List ids = queryOutput.getRecords().stream().map(r -> r.getValue(table.getPrimaryKeyField())).toList(); + List> qPossibleValues = possibleValueTranslator.buildTranslatedPossibleValueList(possibleValueSource, ids); + output.setResults(qPossibleValues); + + return (output); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private SearchPossibleValueSourceOutput searchPossibleValueCustom(SearchPossibleValueSourceInput input, QPossibleValueSource possibleValueSource) + { + try + { + // QCustomPossibleValueProvider customPossibleValueProvider = QCodeLoader.getCustomPossibleValueProvider(possibleValueSource); + // return (formatPossibleValue(possibleValueSource, customPossibleValueProvider.getPossibleValue(value))); + } + catch(Exception e) + { + // LOG.warn("Error sending [" + value + "] for field [" + field + "] through custom code for PVS [" + field.getPossibleValueSourceName() + "]", e); + } + + throw new NotImplementedException("Not impleemnted"); + // return (null); + } + +} 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 39399433..0cb5d6ae 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 @@ -121,6 +121,11 @@ public class QInstanceEnricher { qInstance.getReports().values().forEach(this::enrichReport); } + + if(qInstance.getPossibleValueSources() != null) + { + qInstance.getPossibleValueSources().values().forEach(this::enrichPossibleValueSource); + } } @@ -748,4 +753,50 @@ public class QInstanceEnricher } } + + + /******************************************************************************* + ** + *******************************************************************************/ + private void enrichPossibleValueSource(QPossibleValueSource possibleValueSource) + { + if(QPossibleValueSourceType.TABLE.equals(possibleValueSource.getType())) + { + if(CollectionUtils.nullSafeIsEmpty(possibleValueSource.getSearchFields())) + { + QTableMetaData table = qInstance.getTable(possibleValueSource.getTableName()); + if(table != null) + { + if(table.getPrimaryKeyField() != null) + { + possibleValueSource.withSearchField(table.getPrimaryKeyField()); + } + + for(String recordLabelField : CollectionUtils.nonNullList(table.getRecordLabelFields())) + { + possibleValueSource.withSearchField(recordLabelField); + } + } + } + + if(CollectionUtils.nullSafeIsEmpty(possibleValueSource.getOrderByFields())) + { + QTableMetaData table = qInstance.getTable(possibleValueSource.getTableName()); + if(table != null) + { + for(String recordLabelField : CollectionUtils.nonNullList(table.getRecordLabelFields())) + { + possibleValueSource.withOrderByField(recordLabelField); + } + + if(table.getPrimaryKeyField() != null) + { + possibleValueSource.withOrderByField(table.getPrimaryKeyField()); + } + } + } + + } + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java index 262b6a42..70074957 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java @@ -37,6 +37,7 @@ import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers; import com.kingsrook.qqq.backend.core.actions.processes.BackendStep; import com.kingsrook.qqq.backend.core.actions.values.QCustomPossibleValueProvider; import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy; 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.code.QCodeType; @@ -715,6 +716,8 @@ public class QInstanceValidator case ENUM -> { assertCondition(!StringUtils.hasContent(possibleValueSource.getTableName()), "enum-type possibleValueSource " + pvsName + " should not have a tableName."); + assertCondition(!CollectionUtils.nullSafeHasContents(possibleValueSource.getSearchFields()), "enum-type possibleValueSource " + pvsName + " should not have searchFields."); + assertCondition(!CollectionUtils.nullSafeHasContents(possibleValueSource.getOrderByFields()), "enum-type possibleValueSource " + pvsName + " should not have orderByFields."); assertCondition(possibleValueSource.getCustomCodeReference() == null, "enum-type possibleValueSource " + pvsName + " should not have a customCodeReference."); assertCondition(CollectionUtils.nullSafeHasContents(possibleValueSource.getEnumValues()), "enum-type possibleValueSource " + pvsName + " is missing enum values"); @@ -724,15 +727,44 @@ public class QInstanceValidator assertCondition(CollectionUtils.nullSafeIsEmpty(possibleValueSource.getEnumValues()), "table-type possibleValueSource " + pvsName + " should not have enum values."); assertCondition(possibleValueSource.getCustomCodeReference() == null, "table-type possibleValueSource " + pvsName + " should not have a customCodeReference."); + QTableMetaData tableMetaData = null; if(assertCondition(StringUtils.hasContent(possibleValueSource.getTableName()), "table-type possibleValueSource " + pvsName + " is missing a tableName.")) { - assertCondition(qInstance.getTable(possibleValueSource.getTableName()) != null, "Unrecognized table " + possibleValueSource.getTableName() + " for possibleValueSource " + pvsName + "."); + tableMetaData = qInstance.getTable(possibleValueSource.getTableName()); + assertCondition(tableMetaData != null, "Unrecognized table " + possibleValueSource.getTableName() + " for possibleValueSource " + pvsName + "."); + } + + if(assertCondition(CollectionUtils.nullSafeHasContents(possibleValueSource.getSearchFields()), "table-type possibleValueSource " + pvsName + " is missing searchFields.")) + { + if(tableMetaData != null) + { + QTableMetaData finalTableMetaData = tableMetaData; + for(String searchField : possibleValueSource.getSearchFields()) + { + assertNoException(() -> finalTableMetaData.getField(searchField), "possibleValueSource " + pvsName + " has an unrecognized searchField: " + searchField); + } + } + } + + if(assertCondition(CollectionUtils.nullSafeHasContents(possibleValueSource.getOrderByFields()), "table-type possibleValueSource " + pvsName + " is missing orderByFields.")) + { + if(tableMetaData != null) + { + QTableMetaData finalTableMetaData = tableMetaData; + + for(QFilterOrderBy orderByField : possibleValueSource.getOrderByFields()) + { + assertNoException(() -> finalTableMetaData.getField(orderByField.getFieldName()), "possibleValueSource " + pvsName + " has an unrecognized orderByField: " + orderByField.getFieldName()); + } + } } } case CUSTOM -> { assertCondition(CollectionUtils.nullSafeIsEmpty(possibleValueSource.getEnumValues()), "custom-type possibleValueSource " + pvsName + " should not have enum values."); assertCondition(!StringUtils.hasContent(possibleValueSource.getTableName()), "custom-type possibleValueSource " + pvsName + " should not have a tableName."); + assertCondition(!CollectionUtils.nullSafeHasContents(possibleValueSource.getSearchFields()), "custom-type possibleValueSource " + pvsName + " should not have searchFields."); + assertCondition(!CollectionUtils.nullSafeHasContents(possibleValueSource.getOrderByFields()), "custom-type possibleValueSource " + pvsName + " should not have orderByFields."); if(assertCondition(possibleValueSource.getCustomCodeReference() != null, "custom-type possibleValueSource " + pvsName + " is missing a customCodeReference.")) { diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/values/SearchPossibleValueSourceInput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/values/SearchPossibleValueSourceInput.java new file mode 100644 index 00000000..9ae171bc --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/values/SearchPossibleValueSourceInput.java @@ -0,0 +1,268 @@ +/* + * 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.actions.values; + + +import java.io.Serializable; +import java.util.List; +import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; + + +/******************************************************************************* + ** Input for the Search possible value source action + *******************************************************************************/ +public class SearchPossibleValueSourceInput extends AbstractActionInput +{ + private String possibleValueSourceName; + private QQueryFilter defaultQueryFilter; + private String searchTerm; + private List idList; + + private Integer skip = 0; + private Integer limit = 100; + + + + /******************************************************************************* + ** + *******************************************************************************/ + public SearchPossibleValueSourceInput() + { + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public SearchPossibleValueSourceInput(QInstance instance) + { + super(instance); + } + + + + /******************************************************************************* + ** Getter for possibleValueSourceName + ** + *******************************************************************************/ + public String getPossibleValueSourceName() + { + return possibleValueSourceName; + } + + + + /******************************************************************************* + ** Setter for possibleValueSourceName + ** + *******************************************************************************/ + public void setPossibleValueSourceName(String possibleValueSourceName) + { + this.possibleValueSourceName = possibleValueSourceName; + } + + + + /******************************************************************************* + ** Fluent setter for possibleValueSourceName + ** + *******************************************************************************/ + public SearchPossibleValueSourceInput withPossibleValueSourceName(String possibleValueSourceName) + { + this.possibleValueSourceName = possibleValueSourceName; + return (this); + } + + + + /******************************************************************************* + ** Getter for defaultQueryFilter + ** + *******************************************************************************/ + public QQueryFilter getDefaultQueryFilter() + { + return defaultQueryFilter; + } + + + + /******************************************************************************* + ** Setter for defaultQueryFilter + ** + *******************************************************************************/ + public void setDefaultQueryFilter(QQueryFilter defaultQueryFilter) + { + this.defaultQueryFilter = defaultQueryFilter; + } + + + + /******************************************************************************* + ** Fluent setter for defaultQueryFilter + ** + *******************************************************************************/ + public SearchPossibleValueSourceInput withDefaultQueryFilter(QQueryFilter defaultQueryFilter) + { + this.defaultQueryFilter = defaultQueryFilter; + return (this); + } + + + + /******************************************************************************* + ** Getter for searchTerm + ** + *******************************************************************************/ + public String getSearchTerm() + { + return searchTerm; + } + + + + /******************************************************************************* + ** Setter for searchTerm + ** + *******************************************************************************/ + public void setSearchTerm(String searchTerm) + { + this.searchTerm = searchTerm; + } + + + + /******************************************************************************* + ** Fluent setter for searchTerm + ** + *******************************************************************************/ + public SearchPossibleValueSourceInput withSearchTerm(String searchTerm) + { + this.searchTerm = searchTerm; + return (this); + } + + + + /******************************************************************************* + ** Getter for idList + ** + *******************************************************************************/ + public List getIdList() + { + return idList; + } + + + + /******************************************************************************* + ** Setter for idList + ** + *******************************************************************************/ + public void setIdList(List idList) + { + this.idList = idList; + } + + + + /******************************************************************************* + ** Fluent setter for idList + ** + *******************************************************************************/ + public SearchPossibleValueSourceInput withIdList(List idList) + { + this.idList = idList; + return (this); + } + + + + /******************************************************************************* + ** Getter for skip + ** + *******************************************************************************/ + public Integer getSkip() + { + return skip; + } + + + + /******************************************************************************* + ** Setter for skip + ** + *******************************************************************************/ + public void setSkip(Integer skip) + { + this.skip = skip; + } + + + + /******************************************************************************* + ** Fluent setter for skip + ** + *******************************************************************************/ + public SearchPossibleValueSourceInput withSkip(Integer skip) + { + this.skip = skip; + return (this); + } + + + + /******************************************************************************* + ** Getter for limit + ** + *******************************************************************************/ + public Integer getLimit() + { + return limit; + } + + + + /******************************************************************************* + ** Setter for limit + ** + *******************************************************************************/ + public void setLimit(Integer limit) + { + this.limit = limit; + } + + + + /******************************************************************************* + ** Fluent setter for limit + ** + *******************************************************************************/ + public SearchPossibleValueSourceInput withLimit(Integer limit) + { + this.limit = limit; + return (this); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/values/SearchPossibleValueSourceOutput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/values/SearchPossibleValueSourceOutput.java new file mode 100644 index 00000000..e7186614 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/values/SearchPossibleValueSourceOutput.java @@ -0,0 +1,91 @@ +/* + * 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.actions.values; + + +import java.util.ArrayList; +import java.util.List; +import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput; +import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValue; + + +/******************************************************************************* + ** Output for the Search possible value source action + *******************************************************************************/ +public class SearchPossibleValueSourceOutput extends AbstractActionOutput +{ + private List> results = new ArrayList<>(); + + + + /******************************************************************************* + ** + *******************************************************************************/ + public SearchPossibleValueSourceOutput() + { + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public void addResult(QPossibleValue possibleValue) + { + results.add(possibleValue); + } + + + + /******************************************************************************* + ** Getter for results + ** + *******************************************************************************/ + public List> getResults() + { + return results; + } + + + + /******************************************************************************* + ** Setter for results + ** + *******************************************************************************/ + public void setResults(List> results) + { + this.results = results; + } + + + + /******************************************************************************* + ** Fluent setter for results + ** + *******************************************************************************/ + public SearchPossibleValueSourceOutput withResults(List> results) + { + this.results = results; + return (this); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/possiblevalues/QPossibleValueSource.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/possiblevalues/QPossibleValueSource.java index 4b15b193..accce041 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/possiblevalues/QPossibleValueSource.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/possiblevalues/QPossibleValueSource.java @@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.core.model.metadata.possiblevalues; import java.util.ArrayList; import java.util.List; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; @@ -48,8 +49,9 @@ public class QPossibleValueSource ////////////////////// // for type = TABLE // ////////////////////// - private String tableName; - // todo - override labelFormat & labelFields? + private String tableName; + private List searchFields; + private List orderByFields; ///////////////////// // for type = ENUM // @@ -304,6 +306,128 @@ public class QPossibleValueSource + /******************************************************************************* + ** Getter for searchFields + ** + *******************************************************************************/ + public List getSearchFields() + { + return searchFields; + } + + + + /******************************************************************************* + ** Setter for searchFields + ** + *******************************************************************************/ + public void setSearchFields(List searchFields) + { + this.searchFields = searchFields; + } + + + + /******************************************************************************* + ** Fluent setter for searchFields + ** + *******************************************************************************/ + public QPossibleValueSource withSearchFields(List searchFields) + { + this.searchFields = searchFields; + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for searchFields + ** + *******************************************************************************/ + public QPossibleValueSource withSearchField(String searchField) + { + if(this.searchFields == null) + { + this.searchFields = new ArrayList<>(); + } + this.searchFields.add(searchField); + return (this); + } + + + + /******************************************************************************* + ** Getter for orderByFields + ** + *******************************************************************************/ + public List getOrderByFields() + { + return orderByFields; + } + + + + /******************************************************************************* + ** Setter for orderByFields + ** + *******************************************************************************/ + public void setOrderByFields(List orderByFields) + { + this.orderByFields = orderByFields; + } + + + + /******************************************************************************* + ** Fluent setter for orderByFields + ** + *******************************************************************************/ + public QPossibleValueSource withOrderByFields(List orderByFields) + { + this.orderByFields = orderByFields; + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for orderByFields + ** + *******************************************************************************/ + public QPossibleValueSource withOrderByField(QFilterOrderBy orderByField) + { + if(this.orderByFields == null) + { + this.orderByFields = new ArrayList<>(); + } + this.orderByFields.add(orderByField); + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for orderByFields - default to ASCENDING + ** + *******************************************************************************/ + public QPossibleValueSource withOrderByField(String fieldName) + { + return (withOrderByField(new QFilterOrderBy(fieldName))); + } + + + + /******************************************************************************* + ** Fluent setter for orderByFields + ** + *******************************************************************************/ + public QPossibleValueSource withOrderByField(String fieldName, boolean isAscending) + { + return (withOrderByField(new QFilterOrderBy(fieldName, isAscending))); + } + + + /******************************************************************************* ** Getter for enumValues ** diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java index 894a842e..54370e6f 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java @@ -364,7 +364,17 @@ public class MemoryRecordStore return (false); } - if(!value.equals(criterion.getValues().get(0))) + Serializable criteriaValue = criterion.getValues().get(0); + if(value instanceof String && criteriaValue instanceof Number) + { + criteriaValue = String.valueOf(criteriaValue); + } + else if(criteriaValue instanceof String && value instanceof Number) + { + value = String.valueOf(value); + } + + if(!value.equals(criteriaValue)) { return (false); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ValueUtils.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ValueUtils.java index e91ef363..1970e360 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ValueUtils.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ValueUtils.java @@ -454,7 +454,14 @@ public class ValueUtils return (null); } - return Instant.parse(s); + try + { + return Instant.parse(s); + } + catch(DateTimeParseException e) + { + return tryAlternativeInstantParsing(s, e); + } } else { @@ -473,6 +480,26 @@ public class ValueUtils + /******************************************************************************* + ** + *******************************************************************************/ + private static Instant tryAlternativeInstantParsing(String s, DateTimeParseException e) + { + if(s.matches("^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}$")) + { + ////////////////////////// + // todo ... time zone?? // + ////////////////////////// + return Instant.parse(s + ":00Z"); + } + else + { + throw (e); + } + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/values/SearchPossibleValueSourceActionTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/values/SearchPossibleValueSourceActionTest.java new file mode 100644 index 00000000..7e66f51d --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/values/SearchPossibleValueSourceActionTest.java @@ -0,0 +1,243 @@ +/* + * 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.actions.values; + + +import java.io.Serializable; +import java.util.List; +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.actions.values.SearchPossibleValueSourceOutput; +import com.kingsrook.qqq.backend.core.model.session.QSession; +import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryRecordStore; +import com.kingsrook.qqq.backend.core.utils.TestUtils; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; + + +/******************************************************************************* + ** Unit test for SearchPossibleValueSourceAction + *******************************************************************************/ +class SearchPossibleValueSourceActionTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @BeforeEach + @AfterEach + void beforeAndAfterEach() throws QException + { + MemoryRecordStore.getInstance().reset(); + TestUtils.insertDefaultShapes(TestUtils.defineInstance()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = { " " }) + void testSearchPvsAction_enumNullAndEmptySearchTerms(String searchTerm) throws QException + { + SearchPossibleValueSourceOutput output = getSearchPossibleValueSourceOutput(searchTerm, TestUtils.POSSIBLE_VALUE_SOURCE_STATE); + assertEquals(2, output.getResults().size()); + assertThat(output.getResults()).anyMatch(pv -> pv.getId().equals(1) && pv.getLabel().equals("IL")); + assertThat(output.getResults()).anyMatch(pv -> pv.getId().equals(2) && pv.getLabel().equals("MO")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @ParameterizedTest + @ValueSource(strings = { "I", "IL", "1", "i", "iL", "il" }) + void testSearchPvsAction_enumMatchesForIL(String searchTerm) throws QException + { + SearchPossibleValueSourceOutput output = getSearchPossibleValueSourceOutput(searchTerm, TestUtils.POSSIBLE_VALUE_SOURCE_STATE); + assertEquals(1, output.getResults().size()); + assertThat(output.getResults()).anyMatch(pv -> pv.getId().equals(1) && pv.getLabel().equals("IL")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @ParameterizedTest + @ValueSource(strings = { "3", "ILL" }) + void testSearchPvsAction_enumMatchesNothing(String searchTerm) throws QException + { + SearchPossibleValueSourceOutput output = getSearchPossibleValueSourceOutput(searchTerm, TestUtils.POSSIBLE_VALUE_SOURCE_STATE); + assertEquals(0, output.getResults().size()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testSearchPvsAction_enumById() throws QException + { + SearchPossibleValueSourceOutput output = getSearchPossibleValueSourceOutputById(2, TestUtils.POSSIBLE_VALUE_SOURCE_STATE); + assertEquals(1, output.getResults().size()); + assertThat(output.getResults()).anyMatch(pv -> pv.getId().equals(2) && pv.getLabel().equals("MO")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testSearchPvsAction_enumByIdNotFound() throws QException + { + SearchPossibleValueSourceOutput output = getSearchPossibleValueSourceOutputById(-1, TestUtils.POSSIBLE_VALUE_SOURCE_STATE); + assertEquals(0, output.getResults().size()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = { " " }) + void testSearchPvsAction_tableNullAndEmptySearchTerms(String searchTerm) throws QException + { + SearchPossibleValueSourceOutput output = getSearchPossibleValueSourceOutput(searchTerm, TestUtils.POSSIBLE_VALUE_SOURCE_SHAPE); + assertEquals(3, output.getResults().size()); + assertThat(output.getResults()).anyMatch(pv -> pv.getId().equals(1) && pv.getLabel().equals("Triangle")); + assertThat(output.getResults()).anyMatch(pv -> pv.getId().equals(2) && pv.getLabel().equals("Square")); + assertThat(output.getResults()).anyMatch(pv -> pv.getId().equals(3) && pv.getLabel().equals("Circle")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @ParameterizedTest + @ValueSource(strings = { "1", "Triangle" }) + void testSearchPvsAction_tableMatchesOne(String searchTerm) throws QException + { + SearchPossibleValueSourceOutput output = getSearchPossibleValueSourceOutput(searchTerm, TestUtils.POSSIBLE_VALUE_SOURCE_SHAPE); + assertEquals(1, output.getResults().size()); + assertThat(output.getResults()).anyMatch(pv -> pv.getId().equals(1) && pv.getLabel().equals("Triangle")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testSearchPvsAction_tableById() throws QException + { + SearchPossibleValueSourceOutput output = getSearchPossibleValueSourceOutputById(2, TestUtils.POSSIBLE_VALUE_SOURCE_SHAPE); + assertEquals(1, output.getResults().size()); + assertThat(output.getResults()).anyMatch(pv -> pv.getId().equals(2) && pv.getLabel().equals("Square")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testSearchPvsAction_tableByIds() throws QException + { + SearchPossibleValueSourceOutput output = getSearchPossibleValueSourceOutputByIds(List.of(2, 3), TestUtils.POSSIBLE_VALUE_SOURCE_SHAPE); + assertEquals(2, output.getResults().size()); + assertThat(output.getResults()).anyMatch(pv -> pv.getId().equals(2) && pv.getLabel().equals("Square")); + assertThat(output.getResults()).anyMatch(pv -> pv.getId().equals(3) && pv.getLabel().equals("Circle")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testSearchPvsAction_tableByIdNotFound() throws QException + { + SearchPossibleValueSourceOutput output = getSearchPossibleValueSourceOutputById(-1, TestUtils.POSSIBLE_VALUE_SOURCE_SHAPE); + assertEquals(0, output.getResults().size()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private SearchPossibleValueSourceOutput getSearchPossibleValueSourceOutput(String searchTerm, String possibleValueSourceName) throws QException + { + SearchPossibleValueSourceInput input = new SearchPossibleValueSourceInput(TestUtils.defineInstance()); + input.setSession(new QSession()); + input.setSearchTerm(searchTerm); + input.setPossibleValueSourceName(possibleValueSourceName); + SearchPossibleValueSourceOutput output = new SearchPossibleValueSourceAction().execute(input); + return output; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private SearchPossibleValueSourceOutput getSearchPossibleValueSourceOutputById(Serializable id, String possibleValueSourceName) throws QException + { + SearchPossibleValueSourceInput input = new SearchPossibleValueSourceInput(TestUtils.defineInstance()); + input.setSession(new QSession()); + input.setIdList(List.of(id)); + input.setPossibleValueSourceName(possibleValueSourceName); + SearchPossibleValueSourceOutput output = new SearchPossibleValueSourceAction().execute(input); + return output; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private SearchPossibleValueSourceOutput getSearchPossibleValueSourceOutputByIds(List ids, String possibleValueSourceName) throws QException + { + SearchPossibleValueSourceInput input = new SearchPossibleValueSourceInput(TestUtils.defineInstance()); + input.setSession(new QSession()); + input.setIdList(ids); + input.setPossibleValueSourceName(possibleValueSourceName); + SearchPossibleValueSourceOutput output = new SearchPossibleValueSourceAction().execute(input); + return output; + } + +} \ No newline at end of file diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java index 35567ae4..87a79fa7 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java @@ -37,6 +37,7 @@ import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInpu import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy; 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; @@ -793,10 +794,14 @@ class QInstanceValidatorTest assertValidationFailureReasons((qInstance) -> { QPossibleValueSource possibleValueSource = qInstance.getPossibleValueSource(TestUtils.POSSIBLE_VALUE_SOURCE_STATE); possibleValueSource.setTableName("person"); + possibleValueSource.setSearchFields(List.of("id")); + possibleValueSource.setOrderByFields(List.of(new QFilterOrderBy("id"))); possibleValueSource.setCustomCodeReference(new QCodeReference()); possibleValueSource.setEnumValues(null); }, "should not have a tableName", + "should not have searchFields", + "should not have orderByFields", "should not have a customCodeReference", "is missing enum values"); @@ -815,15 +820,22 @@ class QInstanceValidatorTest assertValidationFailureReasons((qInstance) -> { QPossibleValueSource possibleValueSource = qInstance.getPossibleValueSource(TestUtils.POSSIBLE_VALUE_SOURCE_SHAPE); possibleValueSource.setTableName(null); + possibleValueSource.setSearchFields(null); + possibleValueSource.setOrderByFields(new ArrayList<>()); possibleValueSource.setCustomCodeReference(new QCodeReference()); possibleValueSource.setEnumValues(List.of(new QPossibleValue<>("test"))); }, "should not have enum values", "should not have a customCodeReference", - "is missing a tableName"); + "is missing a tableName", + "is missing searchFields", + "is missing orderByFields"); assertValidationFailureReasons((qInstance) -> qInstance.getPossibleValueSource(TestUtils.POSSIBLE_VALUE_SOURCE_SHAPE).setTableName("Not a table"), "Unrecognized table"); + + assertValidationFailureReasons((qInstance) -> qInstance.getPossibleValueSource(TestUtils.POSSIBLE_VALUE_SOURCE_SHAPE).setSearchFields(List.of("id", "notAField", "name")), + "unrecognized searchField: notAField"); } @@ -837,11 +849,15 @@ class QInstanceValidatorTest assertValidationFailureReasons((qInstance) -> { QPossibleValueSource possibleValueSource = qInstance.getPossibleValueSource(TestUtils.POSSIBLE_VALUE_SOURCE_CUSTOM); possibleValueSource.setTableName("person"); + possibleValueSource.setSearchFields(List.of("id")); + possibleValueSource.setOrderByFields(List.of(new QFilterOrderBy("id"))); possibleValueSource.setCustomCodeReference(null); possibleValueSource.setEnumValues(List.of(new QPossibleValue<>("test"))); }, "should not have enum values", "should not have a tableName", + "should not have searchFields", + "should not have orderByFields", "is missing a customCodeReference"); assertValidationFailureReasons((qInstance) -> qInstance.getPossibleValueSource(TestUtils.POSSIBLE_VALUE_SOURCE_CUSTOM).setCustomCodeReference(new QCodeReference()), 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 35039ce7..dcd96a15 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 @@ -331,7 +331,9 @@ public class TestUtils return new QPossibleValueSource() .withName(POSSIBLE_VALUE_SOURCE_SHAPE) .withType(QPossibleValueSourceType.TABLE) - .withTableName(TABLE_NAME_SHAPE); + .withTableName(TABLE_NAME_SHAPE) + .withSearchFields(List.of("id", "name")) + .withOrderByField("name"); } diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java index 41655288..3a6b1013 100644 --- a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java @@ -28,6 +28,7 @@ import java.io.PipedInputStream; import java.io.PipedOutputStream; import java.io.Serializable; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Locale; @@ -46,6 +47,7 @@ import com.kingsrook.qqq.backend.core.actions.tables.DeleteAction; import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction; +import com.kingsrook.qqq.backend.core.actions.values.SearchPossibleValueSourceAction; import com.kingsrook.qqq.backend.core.adapters.QInstanceAdapter; import com.kingsrook.qqq.backend.core.exceptions.QAuthenticationException; import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException; @@ -76,10 +78,13 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput; import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput; import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateOutput; +import com.kingsrook.qqq.backend.core.model.actions.values.SearchPossibleValueSourceInput; +import com.kingsrook.qqq.backend.core.model.actions.values.SearchPossibleValueSourceOutput; import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput; import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetOutput; 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.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.session.QSession; import com.kingsrook.qqq.backend.core.modules.authentication.Auth0AuthenticationModule; @@ -277,6 +282,7 @@ public class QJavalinImplementation get("/count", QJavalinImplementation::dataCount); get("/export", QJavalinImplementation::dataExportWithoutFilename); get("/export/{filename}", QJavalinImplementation::dataExportWithFilename); + get("/possibleValues/{fieldName}", QJavalinImplementation::possibleValues); // todo - add put and/or patch at this level (without a primaryKey) to do a bulk update based on primaryKeys in the records. path("/{primaryKey}", () -> @@ -864,6 +870,64 @@ public class QJavalinImplementation + /******************************************************************************* + ** + *******************************************************************************/ + private static void possibleValues(Context context) + { + try + { + String tableName = context.pathParam("table"); + String fieldName = context.pathParam("fieldName"); + String searchTerm = context.queryParam("searchTerm"); + String ids = context.queryParam("ids"); + + QTableMetaData table = qInstance.getTable(tableName); + if(table == null) + { + throw (new QNotFoundException("Could not find table named " + tableName + " in this instance.")); + } + + QFieldMetaData field; + try + { + field = table.getField(fieldName); + } + catch(Exception e) + { + throw (new QNotFoundException("Could not find field named " + fieldName + " in table " + tableName + ".")); + } + + if(!StringUtils.hasContent(field.getPossibleValueSourceName())) + { + throw (new QNotFoundException("Field " + fieldName + " in table " + tableName + " is not associated with a possible value source.")); + } + + SearchPossibleValueSourceInput input = new SearchPossibleValueSourceInput(qInstance); + setupSession(context, input); + input.setPossibleValueSourceName(field.getPossibleValueSourceName()); + input.setSearchTerm(searchTerm); + + if(StringUtils.hasContent(ids)) + { + List idList = new ArrayList<>(Arrays.asList(ids.split(","))); + input.setIdList(idList); + } + + SearchPossibleValueSourceOutput output = new SearchPossibleValueSourceAction().execute(input); + + Map result = new HashMap<>(); + result.put("options", output.getResults()); + context.result(JsonUtils.toJson(result)); + } + catch(Exception e) + { + handleException(context, e); + } + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java index 4797a655..c54fe78e 100644 --- a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java +++ b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java @@ -497,4 +497,57 @@ class QJavalinImplementationTest extends QJavalinTestBase assertNotNull(jsonObject.getJSONObject("chartData")); } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testPossibleValueUnfiltered() + { + HttpResponse response = Unirest.get(BASE_URL + "/data/person/possibleValues/partnerPersonId").asString(); + assertEquals(200, response.getStatus()); + JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); + assertNotNull(jsonObject); + assertNotNull(jsonObject.getJSONArray("options")); + assertEquals(5, jsonObject.getJSONArray("options").length()); + assertEquals(1, jsonObject.getJSONArray("options").getJSONObject(0).getInt("id")); + assertEquals("Darin Kelkhoff (1)", jsonObject.getJSONArray("options").getJSONObject(0).getString("label")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testPossibleValueWithSearchTerm() + { + HttpResponse response = Unirest.get(BASE_URL + "/data/person/possibleValues/partnerPersonId?searchTerm=Chamber").asString(); + assertEquals(200, response.getStatus()); + JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); + assertNotNull(jsonObject); + assertNotNull(jsonObject.getJSONArray("options")); + assertEquals(1, jsonObject.getJSONArray("options").length()); + assertEquals("Tim Chamberlain (3)", jsonObject.getJSONArray("options").getJSONObject(0).getString("label")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testPossibleValueWithIds() + { + HttpResponse response = Unirest.get(BASE_URL + "/data/person/possibleValues/partnerPersonId?ids=4,5").asString(); + assertEquals(200, response.getStatus()); + JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); + assertNotNull(jsonObject); + assertNotNull(jsonObject.getJSONArray("options")); + assertEquals(2, jsonObject.getJSONArray("options").length()); + assertEquals(4, jsonObject.getJSONArray("options").getJSONObject(0).getInt("id")); + assertEquals(5, jsonObject.getJSONArray("options").getJSONObject(1).getInt("id")); + } + } diff --git a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java index 04cbb11c..98071fc7 100644 --- a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java +++ b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java @@ -25,28 +25,31 @@ package com.kingsrook.qqq.backend.javalin; import java.io.InputStream; import java.sql.Connection; import java.util.List; +import com.kingsrook.qqq.backend.core.actions.processes.BackendStep; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QValueException; -import com.kingsrook.qqq.backend.core.actions.processes.BackendStep; -import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType; -import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData; -import com.kingsrook.qqq.backend.core.processes.implementations.mock.MockBackendStep; 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.modules.authentication.metadata.QAuthenticationMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType; +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.code.QCodeType; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeUsage; +import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData; 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.QInstance; -import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.PVSValueFormatAndFields; +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.processes.QBackendStepMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionInputMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionOutputMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QRecordListMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.modules.authentication.metadata.QAuthenticationMetaData; +import com.kingsrook.qqq.backend.core.processes.implementations.mock.MockBackendStep; import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager; import com.kingsrook.qqq.backend.module.rdbms.jdbc.QueryManager; import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaData; @@ -123,6 +126,7 @@ public class TestUtils qInstance.addProcess(defineProcessSimpleSleep()); qInstance.addProcess(defineProcessScreenThenSleep()); qInstance.addProcess(defineProcessSimpleThrow()); + qInstance.addPossibleValueSource(definePossibleValueSourcePerson()); defineWidgets(qInstance); return (qInstance); } @@ -181,6 +185,8 @@ public class TestUtils return new QTableMetaData() .withName("person") .withLabel("Person") + .withRecordLabelFormat("%s %s") + .withRecordLabelFields("firstName", "lastName") .withBackendName(defineBackend().getName()) .withPrimaryKeyField("id") .withField(new QFieldMetaData("id", QFieldType.INTEGER)) @@ -189,6 +195,7 @@ public class TestUtils .withField(new QFieldMetaData("firstName", QFieldType.STRING).withBackendName("first_name")) .withField(new QFieldMetaData("lastName", QFieldType.STRING).withBackendName("last_name")) .withField(new QFieldMetaData("birthDate", QFieldType.DATE).withBackendName("birth_date")) + .withField(new QFieldMetaData("partnerPersonId", QFieldType.INTEGER).withBackendName("partner_person_id").withPossibleValueSourceName("person")) .withField(new QFieldMetaData("email", QFieldType.STRING)); } @@ -268,6 +275,22 @@ public class TestUtils + /******************************************************************************* + ** + *******************************************************************************/ + private static QPossibleValueSource definePossibleValueSourcePerson() + { + return (new QPossibleValueSource() + .withName("person") + .withType(QPossibleValueSourceType.TABLE) + .withTableName("person") + .withValueFormatAndFields(PVSValueFormatAndFields.LABEL_PARENS_ID) + .withOrderByField("id") + ); + } + + + /******************************************************************************* ** Define a process with just one step that sleeps *******************************************************************************/ diff --git a/qqq-middleware-javalin/src/test/resources/prime-test-database.sql b/qqq-middleware-javalin/src/test/resources/prime-test-database.sql index be858987..22e88d10 100644 --- a/qqq-middleware-javalin/src/test/resources/prime-test-database.sql +++ b/qqq-middleware-javalin/src/test/resources/prime-test-database.sql @@ -29,7 +29,8 @@ CREATE TABLE person first_name VARCHAR(80) NOT NULL, last_name VARCHAR(80) NOT NULL, birth_date DATE, - email VARCHAR(250) NOT NULL + email VARCHAR(250) NOT NULL, + partner_person_id INT ); INSERT INTO person (id, first_name, last_name, birth_date, email) VALUES (1, 'Darin', 'Kelkhoff', '1980-05-31', 'darin.kelkhoff@gmail.com');