diff --git a/pom.xml b/pom.xml index 098a13fe..ea1003a4 100644 --- a/pom.xml +++ b/pom.xml @@ -48,6 +48,7 @@ true true 0.80 + 0.95 @@ -211,6 +212,11 @@ COVEREDRATIO ${coverage.instructionCoveredRatioMinimum} + + CLASS + COVEREDRATIO + ${coverage.classCoveredRatioMinimum} + @@ -255,7 +261,7 @@ echo "------------------------------------------------------------" which xpath > /dev/null 2>&1 if [ "$?" == "0" ]; then echo "Element\nInstructions Missed\nInstruction Coverage\nBranches Missed\nBranch Coverage\nComplexity Missed\nComplexity Hit\nLines Missed\nLines Hit\nMethods Missed\nMethods Hit\nClasses Missed\nClasses Hit\n" > /tmp/$$.headers - xpath -q -e '/html/body/table/tfoot/tr[1]/td/text()' target/site/jacoco/index.html > /tmp/$$.values + xpath -n -q -e '/html/body/table/tfoot/tr[1]/td/text()' target/site/jacoco/index.html > /tmp/$$.values paste /tmp/$$.headers /tmp/$$.values | tail +2 | awk -v FS='\t' '{printf("%-20s %s\n",$1,$2)}' rm /tmp/$$.headers /tmp/$$.values else diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/customizers/QCodeLoader.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/customizers/QCodeLoader.java new file mode 100644 index 00000000..f1774928 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/customizers/QCodeLoader.java @@ -0,0 +1,124 @@ +/* + * 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.customizers; + + +import java.util.Optional; +import java.util.function.Function; +import com.kingsrook.qqq.backend.core.actions.values.QCustomPossibleValueProvider; +import com.kingsrook.qqq.backend.core.exceptions.QException; +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.possiblevalues.QPossibleValueSource; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + + +/******************************************************************************* + ** Utility to load code for running QQQ customizers. + *******************************************************************************/ +public class QCodeLoader +{ + private static final Logger LOG = LogManager.getLogger(QCodeLoader.class); + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static Optional> getTableCustomizerFunction(QTableMetaData table, String customizerName) + { + Optional codeReference = table.getCustomizer(customizerName); + if(codeReference.isPresent()) + { + return (Optional.ofNullable(QCodeLoader.getFunction(codeReference.get()))); + } + return (Optional.empty()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @SuppressWarnings("unchecked") + public static Function getFunction(QCodeReference codeReference) + { + if(codeReference == null) + { + return (null); + } + + if(!codeReference.getCodeType().equals(QCodeType.JAVA)) + { + /////////////////////////////////////////////////////////////////////////////////////// + // todo - 1) support more languages, 2) wrap them w/ java Functions here, 3) profit! // + /////////////////////////////////////////////////////////////////////////////////////// + throw (new IllegalArgumentException("Only JAVA customizers are supported at this time.")); + } + + try + { + Class customizerClass = Class.forName(codeReference.getName()); + return ((Function) customizerClass.getConstructor().newInstance()); + } + catch(Exception e) + { + LOG.error("Error initializing customizer: " + codeReference); + + ////////////////////////////////////////////////////////////////////////////////////////////////////////// + // return null here - under the assumption that during normal run-time operations, we'll never hit here // + // as we'll want to validate all functions in the instance validator at startup time (and IT will throw // + // if it finds an invalid code reference // + ////////////////////////////////////////////////////////////////////////////////////////////////////////// + return (null); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static QCustomPossibleValueProvider getCustomPossibleValueProvider(QPossibleValueSource possibleValueSource) throws QException + { + try + { + Class codeClass = Class.forName(possibleValueSource.getCustomCodeReference().getName()); + Object codeObject = codeClass.getConstructor().newInstance(); + if(!(codeObject instanceof QCustomPossibleValueProvider customPossibleValueProvider)) + { + throw (new QException("The supplied code [" + codeClass.getName() + "] is not an instance of QCustomPossibleValueProvider")); + } + return (customPossibleValueProvider); + } + catch(QException qe) + { + throw (qe); + } + catch(Exception e) + { + throw (new QException("Error getting custom possible value provider for PVS [" + possibleValueSource.getName() + "]", e)); + } + } +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/customizers/TableCustomizer.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/customizers/TableCustomizer.java new file mode 100644 index 00000000..9367a0c1 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/customizers/TableCustomizer.java @@ -0,0 +1,61 @@ +package com.kingsrook.qqq.backend.core.actions.customizers; + + +import java.util.function.Consumer; + + +/******************************************************************************* + ** Object used by TableCustomizers enum (and similar enums in backend modules) + ** to assist with definition and validation of Customizers applied to tables. + *******************************************************************************/ +public class TableCustomizer +{ + private final String role; + private final Class expectedType; + private final Consumer validationFunction; + + + + /******************************************************************************* + ** + *******************************************************************************/ + public TableCustomizer(String role, Class expectedType, Consumer validationFunction) + { + this.role = role; + this.expectedType = expectedType; + this.validationFunction = validationFunction; + } + + + + /******************************************************************************* + ** Getter for role + ** + *******************************************************************************/ + public String getRole() + { + return role; + } + + + + /******************************************************************************* + ** Getter for expectedType + ** + *******************************************************************************/ + public Class getExpectedType() + { + return expectedType; + } + + + + /******************************************************************************* + ** Getter for validationFunction + ** + *******************************************************************************/ + public Consumer getValidationFunction() + { + return validationFunction; + } +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/customizers/TableCustomizers.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/customizers/TableCustomizers.java new file mode 100644 index 00000000..e5899fad --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/customizers/TableCustomizers.java @@ -0,0 +1,85 @@ +package com.kingsrook.qqq.backend.core.actions.customizers; + + +import java.util.function.Function; +import com.kingsrook.qqq.backend.core.model.data.QRecord; + + +/******************************************************************************* + ** Enum definition of possible table customizers - "roles" for custom code that + ** can be applied to tables. + ** + ** Works with TableCustomizer (singular version of this name) objects, during + ** instance validation, to provide validation of the referenced code (and to + ** make such validation from sub-backend-modules possible in the future). + ** + ** The idea of the 3rd argument here is to provide a way that we can enforce + ** the type-parameters for the custom code. E.g., if it's a Function - how + ** can we check at run-time that the type-params are correct? We couldn't find + ** how to do this "reflectively", so we can instead try to run the custom code, + ** passing it objects of the type that this customizer expects, and a validation + ** error will raise upon ClassCastException... This maybe could improve! + *******************************************************************************/ +public enum TableCustomizers +{ + POST_QUERY_RECORD(new TableCustomizer("postQueryRecord", Function.class, ((Object x) -> + { + Function function = (Function) x; + QRecord output = function.apply(new QRecord()); + }))); + + + private final TableCustomizer tableCustomizer; + + + + /******************************************************************************* + ** + *******************************************************************************/ + TableCustomizers(TableCustomizer tableCustomizer) + { + this.tableCustomizer = tableCustomizer; + } + + + + /******************************************************************************* + ** Get the TableCustomer for a given role (e.g., the role used in meta-data, not + ** the enum-constant name). + *******************************************************************************/ + public static TableCustomizers forRole(String name) + { + for(TableCustomizers value : values()) + { + if(value.tableCustomizer.getRole().equals(name)) + { + return (value); + } + } + + return (null); + } + + + + /******************************************************************************* + ** Getter for tableCustomizer + ** + *******************************************************************************/ + public TableCustomizer getTableCustomizer() + { + return tableCustomizer; + } + + + + /******************************************************************************* + ** get the role from the tableCustomizer + ** + *******************************************************************************/ + public String getRole() + { + return (tableCustomizer.getRole()); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/processes/RunBackendStepAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/processes/RunBackendStepAction.java index 7daf8de9..1d242057 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/processes/RunBackendStepAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/processes/RunBackendStepAction.java @@ -215,7 +215,7 @@ public class RunBackendStepAction Object codeObject = codeClass.getConstructor().newInstance(); if(!(codeObject instanceof BackendStep backendStepCodeObject)) { - throw (new QException("The supplied code [" + codeClass.getName() + "] is not an instance of FunctionBody")); + throw (new QException("The supplied code [" + codeClass.getName() + "] is not an instance of BackendStep")); } backendStepCodeObject.run(runBackendStepInput, runBackendStepOutput); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/RecordPipe.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/RecordPipe.java index 3703a841..2ed70d09 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/RecordPipe.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/RecordPipe.java @@ -26,6 +26,7 @@ import java.util.ArrayList; import java.util.List; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.utils.SleepUtils; import org.apache.logging.log4j.LogManager; @@ -42,12 +43,42 @@ public class RecordPipe private ArrayBlockingQueue queue = new ArrayBlockingQueue<>(1_000); + private Consumer> postRecordActions = null; + + ///////////////////////////////////// + // See usage below for explanation // + ///////////////////////////////////// + private List singleRecordListForPostRecordActions = new ArrayList<>(); + /******************************************************************************* - ** Add a record to the pipe - ** Returns true iff the record fit in the pipe; false if the pipe is currently full. + ** Add a record to the pipe. Will block if the pipe is full. *******************************************************************************/ public void addRecord(QRecord record) + { + if(postRecordActions != null) + { + //////////////////////////////////////////////////////////////////////////////////// + // the initial use-case of this method is to call QueryAction.postRecordActions // + // that method requires that the list param be modifiable. Originally we used // + // List.of here - but that is immutable, so, instead use this single-record-list // + // (which we'll create as a field in this class, to avoid always re-constructing) // + //////////////////////////////////////////////////////////////////////////////////// + singleRecordListForPostRecordActions.add(record); + postRecordActions.accept(singleRecordListForPostRecordActions); + record = singleRecordListForPostRecordActions.remove(0); + } + + doAddRecord(record); + } + + + + /******************************************************************************* + ** Private internal version of add record - assumes the postRecordActions have + ** already ran. + *******************************************************************************/ + private void doAddRecord(QRecord record) { boolean offerResult = queue.offer(record); @@ -66,7 +97,15 @@ public class RecordPipe *******************************************************************************/ public void addRecords(List records) { - records.forEach(this::addRecord); + if(postRecordActions != null) + { + postRecordActions.accept(records); + } + + ////////////////////////////////////////////////////////////////////////////////////////////////// + // make sure to go to the private version of doAddRecord - to avoid re-running the post-actions // + ////////////////////////////////////////////////////////////////////////////////////////////////// + records.forEach(this::doAddRecord); } @@ -101,4 +140,14 @@ public class RecordPipe return (queue.size()); } + + + /******************************************************************************* + ** + *******************************************************************************/ + public void setPostRecordActions(Consumer> postRecordActions) + { + this.postRecordActions = postRecordActions; + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/QueryAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/QueryAction.java index eec134f0..55eb17c9 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/QueryAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/QueryAction.java @@ -22,11 +22,18 @@ package com.kingsrook.qqq.backend.core.actions.tables; +import java.util.List; +import java.util.Optional; +import java.util.function.Function; import com.kingsrook.qqq.backend.core.actions.ActionHelper; +import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader; +import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers; +import com.kingsrook.qqq.backend.core.actions.values.QPossibleValueTranslator; import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter; import com.kingsrook.qqq.backend.core.exceptions.QException; 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.data.QRecord; import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher; import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface; @@ -37,6 +44,14 @@ import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface; *******************************************************************************/ public class QueryAction { + private Optional> postQueryRecordCustomizer; + + private QueryInput queryInput; + private QValueFormatter qValueFormatter; + private QPossibleValueTranslator qPossibleValueTranslator; + + + /******************************************************************************* ** *******************************************************************************/ @@ -44,18 +59,58 @@ public class QueryAction { ActionHelper.validateSession(queryInput); + postQueryRecordCustomizer = QCodeLoader.getTableCustomizerFunction(queryInput.getTable(), TableCustomizers.POST_QUERY_RECORD.getRole()); + this.queryInput = queryInput; + + if(queryInput.getRecordPipe() != null) + { + queryInput.getRecordPipe().setPostRecordActions(this::postRecordActions); + } + QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher(); - QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(queryInput.getBackend()); + QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(queryInput.getBackend()); // todo pre-customization - just get to modify the request? QueryOutput queryOutput = qModule.getQueryInterface().execute(queryInput); // todo post-customization - can do whatever w/ the result if you want - if (queryInput.getRecordPipe() == null) + if(queryInput.getRecordPipe() == null) { - QValueFormatter.setDisplayValuesInRecords(queryInput.getTable(), queryOutput.getRecords()); + postRecordActions(queryOutput.getRecords()); } return queryOutput; } + + + /******************************************************************************* + ** Run the necessary actions on a list of records (which must be a mutable list - e.g., + ** not one created via List.of()). This may include setting display values, + ** translating possible values, and running post-record customizations. + *******************************************************************************/ + public void postRecordActions(List records) + { + if(this.postQueryRecordCustomizer.isPresent()) + { + records.replaceAll(t -> postQueryRecordCustomizer.get().apply(t)); + } + + if(queryInput.getShouldGenerateDisplayValues()) + { + if(qValueFormatter == null) + { + qValueFormatter = new QValueFormatter(); + } + qValueFormatter.setDisplayValuesInRecords(queryInput.getTable(), records); + } + + if(queryInput.getShouldTranslatePossibleValues()) + { + if(qPossibleValueTranslator == null) + { + qPossibleValueTranslator = new QPossibleValueTranslator(queryInput.getInstance(), queryInput.getSession()); + } + qPossibleValueTranslator.translatePossibleValuesInRecords(queryInput.getTable(), records); + } + } } 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 new file mode 100644 index 00000000..5f2cdc17 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QCustomPossibleValueProvider.java @@ -0,0 +1,43 @@ +/* + * 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 com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValue; + + +/******************************************************************************* + ** Interface to be implemented by user-defined code that serves as the backing + ** for a CUSTOM type possibleValueSource + *******************************************************************************/ +public interface QCustomPossibleValueProvider +{ + + /******************************************************************************* + ** + *******************************************************************************/ + QPossibleValue getPossibleValue(Serializable idValue); + + // todo - get/search list of possible values + +} 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 new file mode 100644 index 00000000..5c50cf2c --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QPossibleValueTranslator.java @@ -0,0 +1,355 @@ +/* + * 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.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader; +import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; +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.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.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.model.session.QSession; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import com.kingsrook.qqq.backend.core.utils.ListingHash; +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 QPossibleValueTranslator +{ + private static final Logger LOG = LogManager.getLogger(QPossibleValueTranslator.class); + + private final QInstance qInstance; + private final QSession session; + + /////////////////////////////////////////////////////// + // top-level keys are pvsNames (not table names) // + // 2nd-level keys are pkey values from the PVS table // + /////////////////////////////////////////////////////// + private Map> possibleValueCache; + + + + /******************************************************************************* + ** + *******************************************************************************/ + public QPossibleValueTranslator(QInstance qInstance, QSession session) + { + this.qInstance = qInstance; + this.session = session; + + this.possibleValueCache = new HashMap<>(); + } + + + + /******************************************************************************* + ** For a list of records, translate their possible values (populating their display values) + *******************************************************************************/ + public void translatePossibleValuesInRecords(QTableMetaData table, List records) + { + if(records == null) + { + return; + } + + primePvsCache(table, records); + + for(QRecord record : records) + { + for(QFieldMetaData field : table.getFields().values()) + { + if(field.getPossibleValueSourceName() != null) + { + record.setDisplayValue(field.getName(), translatePossibleValue(field, record.getValue(field.getName()))); + } + } + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + String translatePossibleValue(QFieldMetaData field, Serializable value) + { + QPossibleValueSource possibleValueSource = qInstance.getPossibleValueSource(field.getPossibleValueSourceName()); + if(possibleValueSource == null) + { + LOG.error("Missing possible value source named [" + field.getPossibleValueSourceName() + "] when formatting value for field [" + field.getName() + "]"); + return (null); + } + + String resultValue = null; + if(possibleValueSource.getType().equals(QPossibleValueSourceType.ENUM)) + { + resultValue = translatePossibleValueEnum(value, possibleValueSource); + } + else if(possibleValueSource.getType().equals(QPossibleValueSourceType.TABLE)) + { + resultValue = translatePossibleValueTable(field, value, possibleValueSource); + } + else if(possibleValueSource.getType().equals(QPossibleValueSourceType.CUSTOM)) + { + resultValue = translatePossibleValueCustom(field, value, possibleValueSource); + } + else + { + LOG.error("Unrecognized possibleValueSourceType [" + possibleValueSource.getType() + "] in PVS named [" + possibleValueSource.getName() + "] on field [" + field.getName() + "]"); + } + + if(resultValue == null) + { + resultValue = getDefaultForPossibleValue(possibleValueSource, value); + } + + return (resultValue); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private String translatePossibleValueEnum(Serializable value, QPossibleValueSource possibleValueSource) + { + for(QPossibleValue possibleValue : possibleValueSource.getEnumValues()) + { + if(possibleValue.getId().equals(value)) + { + return (formatPossibleValue(possibleValueSource, possibleValue)); + } + } + + return (null); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private String translatePossibleValueTable(QFieldMetaData field, Serializable value, QPossibleValueSource possibleValueSource) + { + ///////////////////////////////// + // null input gets null output // + ///////////////////////////////// + if(value == null) + { + return (null); + } + + ////////////////////////////////////////////////////////////// + // look for cached value - if it's missing, call the primer // + ////////////////////////////////////////////////////////////// + possibleValueCache.putIfAbsent(possibleValueSource.getName(), new HashMap<>()); + Map cacheForPvs = possibleValueCache.get(possibleValueSource.getName()); + if(!cacheForPvs.containsKey(value)) + { + primePvsCache(possibleValueSource.getTableName(), List.of(possibleValueSource), List.of(value)); + } + + return (cacheForPvs.get(value)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private String translatePossibleValueCustom(QFieldMetaData field, Serializable value, 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); + } + + return (null); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private String formatPossibleValue(QPossibleValueSource possibleValueSource, QPossibleValue possibleValue) + { + return (doFormatPossibleValue(possibleValueSource.getValueFormat(), possibleValueSource.getValueFields(), possibleValue.getId(), possibleValue.getLabel())); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private String getDefaultForPossibleValue(QPossibleValueSource possibleValueSource, Serializable value) + { + if(possibleValueSource.getValueFormatIfNotFound() == null) + { + return (null); + } + + return (doFormatPossibleValue(possibleValueSource.getValueFormatIfNotFound(), possibleValueSource.getValueFieldsIfNotFound(), value, null)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @SuppressWarnings("checkstyle:Indentation") + private String doFormatPossibleValue(String formatString, List valueFields, Object id, String label) + { + List values = new ArrayList<>(); + if(valueFields != null) + { + for(String valueField : valueFields) + { + Object value = switch(valueField) + { + case "id" -> id; + case "label" -> label; + default -> throw new IllegalArgumentException("Unexpected value field: " + valueField); + }; + values.add(Objects.requireNonNullElse(value, "")); + } + } + + return (formatString.formatted(values.toArray())); + } + + + + /******************************************************************************* + ** prime the cache (e.g., by doing bulk-queries) for table-based PVS's + ** + ** @param table the table that the records are from + ** @param records the records that have the possible value id's (e.g., foreign keys) + *******************************************************************************/ + void primePvsCache(QTableMetaData table, List records) + { + ListingHash fieldsByPvsTable = new ListingHash<>(); + ListingHash pvsesByTable = new ListingHash<>(); + for(QFieldMetaData field : table.getFields().values()) + { + QPossibleValueSource possibleValueSource = qInstance.getPossibleValueSource(field.getPossibleValueSourceName()); + if(possibleValueSource != null && possibleValueSource.getType().equals(QPossibleValueSourceType.TABLE)) + { + fieldsByPvsTable.add(possibleValueSource.getTableName(), field); + pvsesByTable.add(possibleValueSource.getTableName(), possibleValueSource); + } + } + + for(String tableName : fieldsByPvsTable.keySet()) + { + Set values = new HashSet<>(); + for(QRecord record : records) + { + for(QFieldMetaData field : fieldsByPvsTable.get(tableName)) + { + values.add(record.getValue(field.getName())); + } + } + + primePvsCache(tableName, pvsesByTable.get(tableName), values); + } + } + + + + /******************************************************************************* + ** For a given table, and a list of pkey-values in that table, AND a list of + ** possible value sources based on that table (maybe usually 1, but could be more, + ** e.g., if they had different formatting, or different filters (todo, would that work?) + ** - query for the values in the table, and populate the possibleValueCache. + *******************************************************************************/ + private void primePvsCache(String tableName, List possibleValueSources, Collection values) + { + for(QPossibleValueSource possibleValueSource : possibleValueSources) + { + possibleValueCache.putIfAbsent(possibleValueSource.getName(), new HashMap<>()); + } + + try + { + String primaryKeyField = qInstance.getTable(tableName).getPrimaryKeyField(); + + for(List page : CollectionUtils.getPages(values, 1000)) + { + QueryInput queryInput = new QueryInput(qInstance); + queryInput.setSession(session); + queryInput.setTableName(tableName); + queryInput.setFilter(new QQueryFilter().withCriteria(new QFilterCriteria(primaryKeyField, QCriteriaOperator.IN, page))); + + ///////////////////////////////////////////////////////////////////////////////////////// + // this is needed to get record labels, which are what we use here... unclear if best! // + ///////////////////////////////////////////////////////////////////////////////////////// + queryInput.setShouldGenerateDisplayValues(true); + + QueryOutput queryOutput = new QueryAction().execute(queryInput); + + for(QRecord record : queryOutput.getRecords()) + { + Serializable pkeyValue = record.getValue(primaryKeyField); + for(QPossibleValueSource possibleValueSource : possibleValueSources) + { + QPossibleValue possibleValue = new QPossibleValue<>(pkeyValue, record.getRecordLabel()); + possibleValueCache.get(possibleValueSource.getName()).put(pkeyValue, formatPossibleValue(possibleValueSource, possibleValue)); + } + } + } + } + catch(Exception e) + { + LOG.warn("Error looking up possible values for table [" + tableName + "]", e); + } + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QValueFormatter.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QValueFormatter.java index 4c20ae7f..5ac811a9 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QValueFormatter.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QValueFormatter.java @@ -34,7 +34,8 @@ import org.apache.logging.log4j.Logger; /******************************************************************************* - ** Utility to apply display formats to values for fields + ** Utility to apply display formats to values for records and fields. + ** *******************************************************************************/ public class QValueFormatter { @@ -45,7 +46,7 @@ public class QValueFormatter /******************************************************************************* ** *******************************************************************************/ - public static String formatValue(QFieldMetaData field, Serializable value) + public String formatValue(QFieldMetaData field, Serializable value) { ////////////////////////////////// // null values get null results // @@ -68,6 +69,7 @@ public class QValueFormatter { try { + // todo - revisit if we actually want this - or - if you should get an error if you mis-configure your table this way (ideally during validation!) if(e.getMessage().equals("f != java.lang.Integer")) { return formatValue(field, ValueUtils.getValueAsBigDecimal(value)); @@ -99,7 +101,7 @@ public class QValueFormatter /******************************************************************************* ** Make a string from a table's recordLabelFormat and fields, for a given record. *******************************************************************************/ - public static String formatRecordLabel(QTableMetaData table, QRecord record) + public String formatRecordLabel(QTableMetaData table, QRecord record) { if(!StringUtils.hasContent(table.getRecordLabelFormat())) { @@ -128,7 +130,7 @@ public class QValueFormatter /******************************************************************************* ** Deal with non-happy-path cases for making a record label. *******************************************************************************/ - private static String formatRecordLabelExceptionalCases(QTableMetaData table, QRecord record) + private String formatRecordLabelExceptionalCases(QTableMetaData table, QRecord record) { /////////////////////////////////////////////////////////////////////////////////////// // if there's no record label format, then just return the primary key display value // @@ -156,7 +158,7 @@ public class QValueFormatter /******************************************************************************* ** For a list of records, set their recordLabels and display values *******************************************************************************/ - public static void setDisplayValuesInRecords(QTableMetaData table, List records) + public void setDisplayValuesInRecords(QTableMetaData table, List records) { if(records == null) { @@ -167,11 +169,11 @@ public class QValueFormatter { for(QFieldMetaData field : table.getFields().values()) { - String formattedValue = QValueFormatter.formatValue(field, record.getValue(field.getName())); + String formattedValue = formatValue(field, record.getValue(field.getName())); record.setDisplayValue(field.getName(), formattedValue); } - record.setRecordLabel(QValueFormatter.formatRecordLabel(table, record)); + record.setRecordLabel(formatRecordLabel(table, record)); } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/adapters/CsvToQRecordAdapter.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/adapters/CsvToQRecordAdapter.java index 3d5493a3..98f85478 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/adapters/CsvToQRecordAdapter.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/adapters/CsvToQRecordAdapter.java @@ -95,6 +95,15 @@ public class CsvToQRecordAdapter throw (new IllegalArgumentException("Empty csv value was provided.")); } + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // once, from a DOS csv file (that had come from Excel), we had a "" character (FEFF, Byte-order marker) at the start of a // + // CSV, which caused our first header to not match... So, let us strip away any FEFF or FFFE's at the start of CSV strings. // + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + if(csv.length() > 1 && (csv.charAt(0) == 0xfeff || csv.charAt(0) == 0xfffe)) + { + csv = csv.substring(1); + } + try { /////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -118,7 +127,7 @@ public class CsvToQRecordAdapter // put values from the CSV record into a map of header -> value // ////////////////////////////////////////////////////////////////// Map csvValues = new HashMap<>(); - for(int i = 0; i < headers.size(); i++) + for(int i = 0; i < headers.size() && i < csvRecord.size(); i++) { csvValues.put(headers.get(i), csvRecord.get(i)); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/exceptions/QInstanceValidationException.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/exceptions/QInstanceValidationException.java index 311ec0a8..fa090ca1 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/exceptions/QInstanceValidationException.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/exceptions/QInstanceValidationException.java @@ -57,7 +57,7 @@ public class QInstanceValidationException extends QException { super( (reasons != null && reasons.size() > 0) - ? "Instance validation failed for the following reasons: " + StringUtils.joinWithCommasAndAnd(reasons) + ? "Instance validation failed for the following reasons:\n - " + StringUtils.join("\n - ", reasons) : "Validation failed, but no reasons were provided"); if(reasons != null && reasons.size() > 0) 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 6104f9db..19beae30 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 @@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.instances; import java.util.ArrayList; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Locale; @@ -128,6 +129,11 @@ public class QInstanceEnricher { generateTableFieldSections(table); } + + if(CollectionUtils.nullSafeHasContents(table.getRecordLabelFields()) && !StringUtils.hasContent(table.getRecordLabelFormat())) + { + table.setRecordLabelFormat(String.join(" ", Collections.nCopies(table.getRecordLabelFields().size(), "%s"))); + } } @@ -211,7 +217,7 @@ public class QInstanceEnricher /******************************************************************************* ** *******************************************************************************/ - private String nameToLabel(String name) + static String nameToLabel(String name) { if(!StringUtils.hasContent(name)) { @@ -223,7 +229,21 @@ public class QInstanceEnricher return (name.substring(0, 1).toUpperCase(Locale.ROOT)); } - return (name.substring(0, 1).toUpperCase(Locale.ROOT) + name.substring(1).replaceAll("([A-Z])", " $1")); + String suffix = name.substring(1) + + ////////////////////////////////////////////////////////////////////// + // Put a space before capital letters or numbers embedded in a name // + // e.g., omethingElse -> omething Else; umber1 -> umber 1 // + ////////////////////////////////////////////////////////////////////// + .replaceAll("([A-Z0-9]+)", " $1") + + //////////////////////////////////////////////////////////////// + // put a space between numbers and words that come after them // + // e.g., umber1dad -> number 1 dad // + //////////////////////////////////////////////////////////////// + .replaceAll("([0-9])([A-Za-z])", "$1 $2"); + + return (name.substring(0, 1).toUpperCase(Locale.ROOT) + suffix); } @@ -579,7 +599,7 @@ public class QInstanceEnricher { for(String fieldName : table.getRecordLabelFields()) { - if(!usedFieldNames.contains(fieldName)) + if(!usedFieldNames.contains(fieldName) && table.getFields().containsKey(fieldName)) { identitySection.getFieldNames().add(fieldName); usedFieldNames.add(fieldName); 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 9ae87598..4b7c4c2b 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 @@ -22,13 +22,21 @@ package com.kingsrook.qqq.backend.core.instances; +import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.function.Consumer; +import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers; +import com.kingsrook.qqq.backend.core.actions.values.QCustomPossibleValueProvider; import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException; 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.layout.QAppChildMetaData; import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData; @@ -37,6 +45,8 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; /******************************************************************************* @@ -51,6 +61,11 @@ import com.kingsrook.qqq.backend.core.utils.StringUtils; *******************************************************************************/ public class QInstanceValidator { + private static final Logger LOG = LogManager.getLogger(QInstanceValidator.class); + + private boolean printWarnings = false; + + /******************************************************************************* ** @@ -88,6 +103,7 @@ public class QInstanceValidator validateTables(qInstance, errors); validateProcesses(qInstance, errors); validateApps(qInstance, errors); + validatePossibleValueSources(qInstance, errors); } catch(Exception e) { @@ -167,8 +183,8 @@ public class QInstanceValidator ////////////////////////////////////////// // validate field sections in the table // ////////////////////////////////////////// - Set fieldNamesInSections = new HashSet<>(); - QFieldSection tier1Section = null; + Set fieldNamesInSections = new HashSet<>(); + QFieldSection tier1Section = null; if(table.getSections() != null) { for(QFieldSection section : table.getSections()) @@ -190,12 +206,140 @@ public class QInstanceValidator } } + /////////////////////////////// + // validate the record label // + /////////////////////////////// + if(table.getRecordLabelFields() != null) + { + for(String recordLabelField : table.getRecordLabelFields()) + { + assertCondition(errors, table.getFields().containsKey(recordLabelField), "Table " + tableName + " record label field " + recordLabelField + " is not a field on this table."); + } + } + + if(table.getCustomizers() != null) + { + for(Map.Entry entry : table.getCustomizers().entrySet()) + { + validateTableCustomizer(errors, tableName, entry.getKey(), entry.getValue()); + } + } }); } } + /******************************************************************************* + ** + *******************************************************************************/ + private void validateTableCustomizer(List errors, String tableName, String customizerName, QCodeReference codeReference) + { + String prefix = "Table " + tableName + ", customizer " + customizerName + ": "; + + if(!preAssertionsForCodeReference(errors, codeReference, prefix)) + { + return; + } + + ////////////////////////////////////////////////////////////////////////////// + // make sure (at this time) that it's a java type, then do some java checks // + ////////////////////////////////////////////////////////////////////////////// + if(assertCondition(errors, codeReference.getCodeType().equals(QCodeType.JAVA), prefix + "Only JAVA customizers are supported at this time.")) + { + /////////////////////////////////////// + // make sure the class can be loaded // + /////////////////////////////////////// + Class customizerClass = getClassForCodeReference(errors, codeReference, prefix); + if(customizerClass != null) + { + ////////////////////////////////////////////////// + // make sure the customizer can be instantiated // + ////////////////////////////////////////////////// + Object customizerInstance = getInstanceOfCodeReference(errors, prefix, customizerClass); + + TableCustomizers tableCustomizer = TableCustomizers.forRole(customizerName); + if(tableCustomizer == null) + { + //////////////////////////////////////////////////////////////////////////////////////////////////// + // todo - in the future, load customizers from backend-modules (e.g., FilesystemTableCustomizers) // + //////////////////////////////////////////////////////////////////////////////////////////////////// + warn(prefix + "Unrecognized table customizer name (at least at backend-core level)"); + } + else + { + //////////////////////////////////////////////////////////////////////// + // make sure the customizer instance can be cast to the expected type // + //////////////////////////////////////////////////////////////////////// + if(customizerInstance != null && tableCustomizer.getTableCustomizer().getExpectedType() != null) + { + Object castedObject = getCastedObject(errors, prefix, tableCustomizer.getTableCustomizer().getExpectedType(), customizerInstance); + + Consumer validationFunction = tableCustomizer.getTableCustomizer().getValidationFunction(); + if(castedObject != null && validationFunction != null) + { + try + { + validationFunction.accept(castedObject); + } + catch(ClassCastException e) + { + errors.add(prefix + "Error validating customizer type parameters: " + e.getMessage()); + } + catch(Exception e) + { + /////////////////////////////////////////////////////////////////////////////////////////////////////////// + // mmm, calling customizers w/ random data is expected to often throw, so, this check is iffy at best... // + // if we run into more trouble here, we might consider disabling the whole "validation function" check. // + /////////////////////////////////////////////////////////////////////////////////////////////////////////// + } + } + } + } + } + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private T getCastedObject(List errors, String prefix, Class expectedType, Object customizerInstance) + { + T castedObject = null; + try + { + castedObject = expectedType.cast(customizerInstance); + } + catch(ClassCastException e) + { + errors.add(prefix + "CodeReference could not be casted to the expected type: " + expectedType); + } + return castedObject; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private Object getInstanceOfCodeReference(List errors, String prefix, Class customizerClass) + { + Object customizerInstance = null; + try + { + customizerInstance = customizerClass.getConstructor().newInstance(); + } + catch(InvocationTargetException | InstantiationException | IllegalAccessException | NoSuchMethodException e) + { + errors.add(prefix + "Instance of CodeReference could not be created: " + e); + } + return customizerInstance; + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -225,7 +369,7 @@ public class QInstanceValidator *******************************************************************************/ private void validateProcesses(QInstance qInstance, List errors) { - if(!CollectionUtils.nullSafeIsEmpty(qInstance.getProcesses())) + if(CollectionUtils.nullSafeHasContents(qInstance.getProcesses())) { qInstance.getProcesses().forEach((processName, process) -> { @@ -264,7 +408,7 @@ public class QInstanceValidator *******************************************************************************/ private void validateApps(QInstance qInstance, List errors) { - if(!CollectionUtils.nullSafeIsEmpty(qInstance.getApps())) + if(CollectionUtils.nullSafeHasContents(qInstance.getApps())) { qInstance.getApps().forEach((appName, app) -> { @@ -291,6 +435,142 @@ public class QInstanceValidator + /******************************************************************************* + ** + *******************************************************************************/ + private void validatePossibleValueSources(QInstance qInstance, List errors) + { + if(CollectionUtils.nullSafeHasContents(qInstance.getPossibleValueSources())) + { + qInstance.getPossibleValueSources().forEach((pvsName, possibleValueSource) -> + { + assertCondition(errors, Objects.equals(pvsName, possibleValueSource.getName()), "Inconsistent naming for possibleValueSource: " + pvsName + "/" + possibleValueSource.getName() + "."); + if(assertCondition(errors, possibleValueSource.getType() != null, "Missing type for possibleValueSource: " + pvsName)) + { + //////////////////////////////////////////////////////////////////////////////////////////////// + // assert about fields that should and should not be set, based on possible value source type // + // do additional type-specific validations as well // + //////////////////////////////////////////////////////////////////////////////////////////////// + switch(possibleValueSource.getType()) + { + case ENUM -> + { + assertCondition(errors, !StringUtils.hasContent(possibleValueSource.getTableName()), "enum-type possibleValueSource " + pvsName + " should not have a tableName."); + assertCondition(errors, possibleValueSource.getCustomCodeReference() == null, "enum-type possibleValueSource " + pvsName + " should not have a customCodeReference."); + + assertCondition(errors, CollectionUtils.nullSafeHasContents(possibleValueSource.getEnumValues()), "enum-type possibleValueSource " + pvsName + " is missing enum values"); + } + case TABLE -> + { + assertCondition(errors, CollectionUtils.nullSafeIsEmpty(possibleValueSource.getEnumValues()), "table-type possibleValueSource " + pvsName + " should not have enum values."); + assertCondition(errors, possibleValueSource.getCustomCodeReference() == null, "table-type possibleValueSource " + pvsName + " should not have a customCodeReference."); + + if(assertCondition(errors, StringUtils.hasContent(possibleValueSource.getTableName()), "table-type possibleValueSource " + pvsName + " is missing a tableName.")) + { + assertCondition(errors, qInstance.getTable(possibleValueSource.getTableName()) != null, "Unrecognized table " + possibleValueSource.getTableName() + " for possibleValueSource " + pvsName + "."); + } + } + case CUSTOM -> + { + assertCondition(errors, CollectionUtils.nullSafeIsEmpty(possibleValueSource.getEnumValues()), "custom-type possibleValueSource " + pvsName + " should not have enum values."); + assertCondition(errors, !StringUtils.hasContent(possibleValueSource.getTableName()), "custom-type possibleValueSource " + pvsName + " should not have a tableName."); + + if(assertCondition(errors, possibleValueSource.getCustomCodeReference() != null, "custom-type possibleValueSource " + pvsName + " is missing a customCodeReference.")) + { + assertCondition(errors, QCodeUsage.POSSIBLE_VALUE_PROVIDER.equals(possibleValueSource.getCustomCodeReference().getCodeUsage()), "customCodeReference for possibleValueSource " + pvsName + " is not a possibleValueProvider."); + validateCustomPossibleValueSourceCode(errors, pvsName, possibleValueSource.getCustomCodeReference()); + } + } + default -> errors.add("Unexpected possibleValueSource type: " + possibleValueSource.getType()); + } + } + }); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private void validateCustomPossibleValueSourceCode(List errors, String pvsName, QCodeReference codeReference) + { + String prefix = "PossibleValueSource " + pvsName + " custom code reference: "; + + if(!preAssertionsForCodeReference(errors, codeReference, prefix)) + { + return; + } + + ////////////////////////////////////////////////////////////////////////////// + // make sure (at this time) that it's a java type, then do some java checks // + ////////////////////////////////////////////////////////////////////////////// + if(assertCondition(errors, codeReference.getCodeType().equals(QCodeType.JAVA), prefix + "Only JAVA customizers are supported at this time.")) + { + /////////////////////////////////////// + // make sure the class can be loaded // + /////////////////////////////////////// + Class customizerClass = getClassForCodeReference(errors, codeReference, prefix); + if(customizerClass != null) + { + ////////////////////////////////////////////////// + // make sure the customizer can be instantiated // + ////////////////////////////////////////////////// + Object customizerInstance = getInstanceOfCodeReference(errors, prefix, customizerClass); + + //////////////////////////////////////////////////////////////////////// + // make sure the customizer instance can be cast to the expected type // + //////////////////////////////////////////////////////////////////////// + if(customizerInstance != null) + { + getCastedObject(errors, prefix, QCustomPossibleValueProvider.class, customizerInstance); + } + } + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private Class getClassForCodeReference(List errors, QCodeReference codeReference, String prefix) + { + Class customizerClass = null; + try + { + customizerClass = Class.forName(codeReference.getName()); + } + catch(ClassNotFoundException e) + { + errors.add(prefix + "Class for CodeReference could not be found."); + } + return customizerClass; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private boolean preAssertionsForCodeReference(List errors, QCodeReference codeReference, String prefix) + { + boolean okay = true; + if(!assertCondition(errors, StringUtils.hasContent(codeReference.getName()), prefix + " is missing a code reference name")) + { + okay = false; + } + + if(!assertCondition(errors, codeReference.getCodeType() != null, prefix + " is missing a code type")) + { + okay = false; + } + + return (okay); + } + + + /******************************************************************************* ** Check if an app's child list can recursively be traversed without finding a ** duplicate, which would indicate a cycle (e.g., an error) @@ -343,4 +623,16 @@ public class QInstanceValidator return (condition); } + + + /******************************************************************************* + ** + *******************************************************************************/ + private void warn(String message) + { + if(printWarnings) + { + LOG.info("Validation warning: " + message); + } + } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryInput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryInput.java index daaa5ab7..94c2a7ae 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryInput.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryInput.java @@ -40,6 +40,8 @@ public class QueryInput extends AbstractTableActionInput private RecordPipe recordPipe; + private boolean shouldTranslatePossibleValues = false; + private boolean shouldGenerateDisplayValues = false; /******************************************************************************* @@ -158,4 +160,47 @@ public class QueryInput extends AbstractTableActionInput this.recordPipe = recordPipe; } + + + /******************************************************************************* + ** Getter for shouldTranslatePossibleValues + ** + *******************************************************************************/ + public boolean getShouldTranslatePossibleValues() + { + return shouldTranslatePossibleValues; + } + + + + /******************************************************************************* + ** Setter for shouldTranslatePossibleValues + ** + *******************************************************************************/ + public void setShouldTranslatePossibleValues(boolean shouldTranslatePossibleValues) + { + this.shouldTranslatePossibleValues = shouldTranslatePossibleValues; + } + + + + /******************************************************************************* + ** Getter for shouldGenerateDisplayValues + ** + *******************************************************************************/ + public boolean getShouldGenerateDisplayValues() + { + return shouldGenerateDisplayValues; + } + + + + /******************************************************************************* + ** Setter for shouldGenerateDisplayValues + ** + *******************************************************************************/ + public void setShouldGenerateDisplayValues(boolean shouldGenerateDisplayValues) + { + this.shouldGenerateDisplayValues = shouldGenerateDisplayValues; + } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QField.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QField.java index 6fd3eeb6..1988bd6a 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QField.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QField.java @@ -62,6 +62,11 @@ public @interface QField *******************************************************************************/ String displayFormat() default ""; + /******************************************************************************* + ** + *******************************************************************************/ + String possibleValueSourceName() default ""; + ////////////////////////////////////////////////////////////////////////////////////////// // new attributes here likely need implementation in QFieldMetaData.constructFromGetter // ////////////////////////////////////////////////////////////////////////////////////////// diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecord.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecord.java index 3c30366f..77cdd6e8 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecord.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecord.java @@ -25,6 +25,7 @@ package com.kingsrook.qqq.backend.core.model.data; import java.io.Serializable; import java.math.BigDecimal; import java.time.LocalDate; +import java.time.LocalTime; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; @@ -328,6 +329,16 @@ public class QRecord implements Serializable + /******************************************************************************* + ** + *******************************************************************************/ + public LocalTime getValueLocalTime(String fieldName) + { + return (ValueUtils.getValueAsLocalTime(values.get(fieldName))); + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QInstance.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QInstance.java index 6f4c5230..3712d077 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QInstance.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QInstance.java @@ -54,10 +54,10 @@ public class QInstance //////////////////////////////////////////////////////////////////////////////////////////// // Important to use LinkedHashmap here, to preserve the order in which entries are added. // //////////////////////////////////////////////////////////////////////////////////////////// - private Map tables = new LinkedHashMap<>(); - private Map> possibleValueSources = new LinkedHashMap<>(); - private Map processes = new LinkedHashMap<>(); - private Map apps = new LinkedHashMap<>(); + private Map tables = new LinkedHashMap<>(); + private Map possibleValueSources = new LinkedHashMap<>(); + private Map processes = new LinkedHashMap<>(); + private Map apps = new LinkedHashMap<>(); // todo - lock down the object (no more changes allowed) after it's been validated? @@ -190,7 +190,7 @@ public class QInstance /******************************************************************************* ** *******************************************************************************/ - public void addPossibleValueSource(QPossibleValueSource possibleValueSource) + public void addPossibleValueSource(QPossibleValueSource possibleValueSource) { this.addPossibleValueSource(possibleValueSource.getName(), possibleValueSource); } @@ -353,7 +353,7 @@ public class QInstance ** Getter for possibleValueSources ** *******************************************************************************/ - public Map> getPossibleValueSources() + public Map getPossibleValueSources() { return possibleValueSources; } @@ -364,7 +364,7 @@ public class QInstance ** Setter for possibleValueSources ** *******************************************************************************/ - public void setPossibleValueSources(Map> possibleValueSources) + public void setPossibleValueSources(Map possibleValueSources) { this.possibleValueSources = possibleValueSources; } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/QCodeReference.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/QCodeReference.java index c1093ca9..5a82f004 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/QCodeReference.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/QCodeReference.java @@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.model.metadata.code; import com.kingsrook.qqq.backend.core.actions.processes.BackendStep; +import com.kingsrook.qqq.backend.core.actions.values.QCustomPossibleValueProvider; /******************************************************************************* @@ -70,6 +71,10 @@ public class QCodeReference { this.codeUsage = QCodeUsage.BACKEND_STEP; } + else if(QCustomPossibleValueProvider.class.isAssignableFrom(javaClass)) + { + this.codeUsage = QCodeUsage.POSSIBLE_VALUE_PROVIDER; + } else { throw (new IllegalStateException("Unable to infer code usage type for class: " + javaClass.getName())); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/QCodeUsage.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/QCodeUsage.java index 9b459af1..925c1f55 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/QCodeUsage.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/QCodeUsage.java @@ -29,5 +29,6 @@ package com.kingsrook.qqq.backend.core.model.metadata.code; public enum QCodeUsage { BACKEND_STEP, // a backend-step in a process - CUSTOMIZER // a function to customize part of a QQQ table's behavior + CUSTOMIZER, // a function to customize part of a QQQ table's behavior + POSSIBLE_VALUE_PROVIDER // code that drives a custom possibleValueSource } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldMetaData.java index 8967c579..a21d7cce 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldMetaData.java @@ -133,6 +133,11 @@ public class QFieldMetaData { setDisplayFormat(fieldAnnotation.displayFormat()); } + + if(StringUtils.hasContent(fieldAnnotation.possibleValueSourceName())) + { + setPossibleValueSourceName(fieldAnnotation.possibleValueSourceName()); + } } } catch(QException qe) @@ -406,6 +411,7 @@ public class QFieldMetaData } + /******************************************************************************* ** Getter for displayFormat ** @@ -427,6 +433,7 @@ public class QFieldMetaData } + /******************************************************************************* ** Fluent setter for displayFormat ** diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendFieldMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendFieldMetaData.java index f57a05ff..4dbbcfc6 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendFieldMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendFieldMetaData.java @@ -36,11 +36,13 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; @JsonInclude(Include.NON_NULL) public class QFrontendFieldMetaData { - private String name; - private String label; + private String name; + private String label; private QFieldType type; - private boolean isRequired; - private boolean isEditable; + private boolean isRequired; + private boolean isEditable; + private String possibleValueSourceName; + private String displayFormat; ////////////////////////////////////////////////////////////////////////////////// // do not add setters. take values from the source-object in the constructor!! // @@ -58,6 +60,8 @@ public class QFrontendFieldMetaData this.type = fieldMetaData.getType(); this.isRequired = fieldMetaData.getIsRequired(); this.isEditable = fieldMetaData.getIsEditable(); + this.possibleValueSourceName = fieldMetaData.getPossibleValueSourceName(); + this.displayFormat = fieldMetaData.getDisplayFormat(); } @@ -115,4 +119,26 @@ public class QFrontendFieldMetaData return isEditable; } + + + /******************************************************************************* + ** Getter for displayFormat + ** + *******************************************************************************/ + public String getDisplayFormat() + { + return displayFormat; + } + + + + /******************************************************************************* + ** Getter for possibleValueSourceName + ** + *******************************************************************************/ + public String getPossibleValueSourceName() + { + return possibleValueSourceName; + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/possiblevalues/PVSValueFormatAndFields.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/possiblevalues/PVSValueFormatAndFields.java new file mode 100644 index 00000000..f8ea0eb9 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/possiblevalues/PVSValueFormatAndFields.java @@ -0,0 +1,55 @@ +package com.kingsrook.qqq.backend.core.model.metadata.possiblevalues; + + +import java.util.List; + + +/******************************************************************************* + ** Define some standard ways to format the value portion of a PossibleValueSource. + ** + ** Can be passed to short-cut {set,with}ValueFormatAndFields methods in QPossibleValueSource + ** class, or the format & field properties can be extracted and passed to regular field-level setters. + *******************************************************************************/ +public enum PVSValueFormatAndFields +{ + LABEL_ONLY("%s", "label"), + LABEL_PARENS_ID("%s (%s)", "label", "id"), + ID_COLON_LABEL("%s: %s", "id", "label"); + + + private final String format; + private final List fields; + + + + /******************************************************************************* + ** + *******************************************************************************/ + PVSValueFormatAndFields(String format, String... fields) + { + this.format = format; + this.fields = List.of(fields); + } + + + + /******************************************************************************* + ** Getter for format + ** + *******************************************************************************/ + public String getFormat() + { + return format; + } + + + + /******************************************************************************* + ** Getter for fields + ** + *******************************************************************************/ + public List getFields() + { + return fields; + } +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/possiblevalues/PossibleValueEnum.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/possiblevalues/PossibleValueEnum.java new file mode 100644 index 00000000..fc61b2df --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/possiblevalues/PossibleValueEnum.java @@ -0,0 +1,40 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.model.metadata.possiblevalues; + + +/******************************************************************************* + ** Interface to be implemented by enums which can be used as a PossibleValueSource. + ** + *******************************************************************************/ +public interface PossibleValueEnum +{ + /******************************************************************************* + ** + *******************************************************************************/ + T getPossibleValueId(); + + /******************************************************************************* + ** + *******************************************************************************/ + String getPossibleValueLabel(); +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/possiblevalues/QPossibleValue.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/possiblevalues/QPossibleValue.java new file mode 100644 index 00000000..9bdae034 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/possiblevalues/QPossibleValue.java @@ -0,0 +1,78 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.model.metadata.possiblevalues; + + +/******************************************************************************* + ** An actual possible value - an id and label. + ** + *******************************************************************************/ +public class QPossibleValue +{ + private final T id; + private final String label; + + + + /******************************************************************************* + ** + *******************************************************************************/ + @SuppressWarnings("unchecked") + public QPossibleValue(String value) + { + this.id = (T) value; + this.label = value; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public QPossibleValue(T id, String label) + { + this.id = id; + this.label = label; + } + + + + /******************************************************************************* + ** Getter for id + ** + *******************************************************************************/ + public T getId() + { + return id; + } + + + + /******************************************************************************* + ** Getter for label + ** + *******************************************************************************/ + public String getLabel() + { + return label; + } +} 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 0cb0a322..4b15b193 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,19 +24,42 @@ package com.kingsrook.qqq.backend.core.model.metadata.possiblevalues; import java.util.ArrayList; import java.util.List; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; /******************************************************************************* - ** Meta-data to represent a single field in a table. + ** Meta-data to represent a "Possible value" - e.g., a translation of a foreign + ** key and/or a limited set of "possible values" for a field (e.g., from a foreign + ** table or an enum). ** *******************************************************************************/ -public class QPossibleValueSource +public class QPossibleValueSource { - private String name; + private String name; private QPossibleValueSourceType type; - // should these be in sub-types?? - private List enumValues; + private String valueFormat = PVSValueFormatAndFields.LABEL_ONLY.getFormat(); + private List valueFields = PVSValueFormatAndFields.LABEL_ONLY.getFields(); + private String valueFormatIfNotFound = null; + private List valueFieldsIfNotFound = null; + + // todo - optimization hints, such as "table is static, fully cache" or "table is small, so we can pull the whole thing into memory?" + + ////////////////////// + // for type = TABLE // + ////////////////////// + private String tableName; + // todo - override labelFormat & labelFields? + + ///////////////////// + // for type = ENUM // + ///////////////////// + private List> enumValues; + + /////////////////////// + // for type = CUSTOM // + /////////////////////// + private QCodeReference customCodeReference; @@ -72,7 +95,7 @@ public class QPossibleValueSource /******************************************************************************* ** *******************************************************************************/ - public QPossibleValueSource withName(String name) + public QPossibleValueSource withName(String name) { this.name = name; return (this); @@ -103,7 +126,7 @@ public class QPossibleValueSource /******************************************************************************* ** *******************************************************************************/ - public QPossibleValueSource withType(QPossibleValueSourceType type) + public QPossibleValueSource withType(QPossibleValueSourceType type) { this.type = type; return (this); @@ -111,11 +134,181 @@ public class QPossibleValueSource + /******************************************************************************* + ** Getter for valueFormat + ** + *******************************************************************************/ + public String getValueFormat() + { + return valueFormat; + } + + + + /******************************************************************************* + ** Setter for valueFormat + ** + *******************************************************************************/ + public void setValueFormat(String valueFormat) + { + this.valueFormat = valueFormat; + } + + + + /******************************************************************************* + ** Fluent setter for valueFormat + ** + *******************************************************************************/ + public QPossibleValueSource withValueFormat(String valueFormat) + { + this.valueFormat = valueFormat; + return (this); + } + + + + /******************************************************************************* + ** Getter for valueFields + ** + *******************************************************************************/ + public List getValueFields() + { + return valueFields; + } + + + + /******************************************************************************* + ** Setter for valueFields + ** + *******************************************************************************/ + public void setValueFields(List valueFields) + { + this.valueFields = valueFields; + } + + + + /******************************************************************************* + ** Fluent setter for valueFields + ** + *******************************************************************************/ + public QPossibleValueSource withValueFields(List valueFields) + { + this.valueFields = valueFields; + return (this); + } + + + + /******************************************************************************* + ** Getter for valueFormatIfNotFound + ** + *******************************************************************************/ + public String getValueFormatIfNotFound() + { + return valueFormatIfNotFound; + } + + + + /******************************************************************************* + ** Setter for valueFormatIfNotFound + ** + *******************************************************************************/ + public void setValueFormatIfNotFound(String valueFormatIfNotFound) + { + this.valueFormatIfNotFound = valueFormatIfNotFound; + } + + + + /******************************************************************************* + ** Fluent setter for valueFormatIfNotFound + ** + *******************************************************************************/ + public QPossibleValueSource withValueFormatIfNotFound(String valueFormatIfNotFound) + { + this.valueFormatIfNotFound = valueFormatIfNotFound; + return (this); + } + + + + /******************************************************************************* + ** Getter for valueFieldsIfNotFound + ** + *******************************************************************************/ + public List getValueFieldsIfNotFound() + { + return valueFieldsIfNotFound; + } + + + + /******************************************************************************* + ** Setter for valueFieldsIfNotFound + ** + *******************************************************************************/ + public void setValueFieldsIfNotFound(List valueFieldsIfNotFound) + { + this.valueFieldsIfNotFound = valueFieldsIfNotFound; + } + + + + /******************************************************************************* + ** Fluent setter for valueFieldsIfNotFound + ** + *******************************************************************************/ + public QPossibleValueSource withValueFieldsIfNotFound(List valueFieldsIfNotFound) + { + this.valueFieldsIfNotFound = valueFieldsIfNotFound; + return (this); + } + + + + /******************************************************************************* + ** Getter for tableName + ** + *******************************************************************************/ + public String getTableName() + { + return tableName; + } + + + + /******************************************************************************* + ** Setter for tableName + ** + *******************************************************************************/ + public void setTableName(String tableName) + { + this.tableName = tableName; + } + + + + /******************************************************************************* + ** Fluent setter for tableName + ** + *******************************************************************************/ + public QPossibleValueSource withTableName(String tableName) + { + this.tableName = tableName; + return (this); + } + + + /******************************************************************************* ** Getter for enumValues ** *******************************************************************************/ - public List getEnumValues() + public List> getEnumValues() { return enumValues; } @@ -126,7 +319,7 @@ public class QPossibleValueSource ** Setter for enumValues ** *******************************************************************************/ - public void setEnumValues(List enumValues) + public void setEnumValues(List> enumValues) { this.enumValues = enumValues; } @@ -137,7 +330,7 @@ public class QPossibleValueSource ** Fluent setter for enumValues ** *******************************************************************************/ - public QPossibleValueSource withEnumValues(List enumValues) + public QPossibleValueSource withEnumValues(List> enumValues) { this.enumValues = enumValues; return this; @@ -146,16 +339,89 @@ public class QPossibleValueSource /******************************************************************************* - ** Fluent adder for enumValues ** *******************************************************************************/ - public QPossibleValueSource addEnumValue(T enumValue) + public void addEnumValue(QPossibleValue possibleValue) { if(this.enumValues == null) { this.enumValues = new ArrayList<>(); } - this.enumValues.add(enumValue); - return this; + this.enumValues.add(possibleValue); } + + + + /******************************************************************************* + ** This is the easiest way to add the values from an enum to a PossibleValueSource. + ** Make sure the enum implements PossibleValueEnum - then call as: + ** myPossibleValueSource.withValuesFromEnum(MyEnum.values())); + ** + *******************************************************************************/ + public > QPossibleValueSource withValuesFromEnum(T[] values) + { + for(T t : values) + { + addEnumValue(new QPossibleValue<>(t.getPossibleValueId(), t.getPossibleValueLabel())); + } + + return (this); + } + + + + /******************************************************************************* + ** Getter for customCodeReference + ** + *******************************************************************************/ + public QCodeReference getCustomCodeReference() + { + return customCodeReference; + } + + + + /******************************************************************************* + ** Setter for customCodeReference + ** + *******************************************************************************/ + public void setCustomCodeReference(QCodeReference customCodeReference) + { + this.customCodeReference = customCodeReference; + } + + + + /******************************************************************************* + ** Fluent setter for customCodeReference + ** + *******************************************************************************/ + public QPossibleValueSource withCustomCodeReference(QCodeReference customCodeReference) + { + this.customCodeReference = customCodeReference; + return (this); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public void setValueFormatAndFields(PVSValueFormatAndFields valueFormatAndFields) + { + this.valueFormat = valueFormatAndFields.getFormat(); + this.valueFields = valueFormatAndFields.getFields(); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public QPossibleValueSource withValueFormatAndFields(PVSValueFormatAndFields valueFormatAndFields) + { + setValueFormatAndFields(valueFormatAndFields); + return (this); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QTableMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QTableMetaData.java index 0fec3d48..0d1cbcef 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QTableMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QTableMetaData.java @@ -24,11 +24,13 @@ package com.kingsrook.qqq.backend.core.model.metadata.tables; import java.io.Serializable; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; +import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizer; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.data.QRecordEntity; import com.kingsrook.qqq.backend.core.model.data.QRecordEntityField; @@ -84,6 +86,17 @@ public class QTableMetaData implements QAppChildMetaData, Serializable + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public String toString() + { + return ("QTableMetaData[" + name + "]"); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -408,12 +421,7 @@ public class QTableMetaData implements QAppChildMetaData, Serializable } QCodeReference function = customizers.get(customizerName); - if(function == null) - { - throw (new IllegalArgumentException("Customizer [" + customizerName + "] was not found in table [" + name + "].")); - } - - return (Optional.of(function)); + return (Optional.ofNullable(function)); } @@ -455,6 +463,16 @@ public class QTableMetaData implements QAppChildMetaData, Serializable + /******************************************************************************* + ** + *******************************************************************************/ + public QTableMetaData withCustomizer(TableCustomizer tableCustomizer, QCodeReference customizer) + { + return (withCustomizer(tableCustomizer.getRole(), customizer)); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -592,6 +610,18 @@ public class QTableMetaData implements QAppChildMetaData, Serializable + /******************************************************************************* + ** Fluent setter for recordLabelFields + ** + *******************************************************************************/ + public QTableMetaData withRecordLabelFields(String... recordLabelFields) + { + this.recordLabelFields = Arrays.asList(recordLabelFields); + return (this); + } + + + /******************************************************************************* ** Getter for sections ** diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/Auth0AuthenticationModule.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/Auth0AuthenticationModule.java index 435a13ce..6b53dd6b 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/Auth0AuthenticationModule.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/Auth0AuthenticationModule.java @@ -167,6 +167,11 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface return (false); } + if(session.getIdReference() == null) + { + return (false); + } + StateProviderInterface spi = getStateProvider(); Auth0StateKey key = new Auth0StateKey(session.getIdReference()); Optional lastTimeCheckedOptional = spi.get(Instant.class, key); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/QBackendModuleDispatcher.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/QBackendModuleDispatcher.java index c57afd5b..19161379 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/QBackendModuleDispatcher.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/QBackendModuleDispatcher.java @@ -72,6 +72,7 @@ public class QBackendModuleDispatcher // todo - let modules somehow "export" their types here? // e.g., backend-core shouldn't need to "know" about the modules. "com.kingsrook.qqq.backend.core.modules.backend.implementations.mock.MockBackendModule", + "com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryBackendModule", "com.kingsrook.qqq.backend.module.rdbms.RDBMSBackendModule", "com.kingsrook.qqq.backend.module.filesystem.local.FilesystemBackendModule", "com.kingsrook.qqq.backend.module.filesystem.s3.S3BackendModule" diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryBackendModule.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryBackendModule.java new file mode 100644 index 00000000..84852cb9 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryBackendModule.java @@ -0,0 +1,116 @@ +/* + * 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.modules.backend.implementations.memory; + + +import com.kingsrook.qqq.backend.core.actions.interfaces.CountInterface; +import com.kingsrook.qqq.backend.core.actions.interfaces.DeleteInterface; +import com.kingsrook.qqq.backend.core.actions.interfaces.InsertInterface; +import com.kingsrook.qqq.backend.core.actions.interfaces.QueryInterface; +import com.kingsrook.qqq.backend.core.actions.interfaces.UpdateInterface; +import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData; +import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface; + + +/******************************************************************************* + ** A simple (probably only valid for testing?) implementation of the QModuleInterface, + ** that just stores its records in-memory. + ** + *******************************************************************************/ +public class MemoryBackendModule implements QBackendModuleInterface +{ + /******************************************************************************* + ** Method where a backend module must be able to provide its type (name). + *******************************************************************************/ + @Override + public String getBackendType() + { + return ("memory"); + } + + + + /******************************************************************************* + ** Method to identify the class used for backend meta data for this module. + *******************************************************************************/ + @Override + public Class getBackendMetaDataClass() + { + return (QBackendMetaData.class); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public CountInterface getCountInterface() + { + return new MemoryCountAction(); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public QueryInterface getQueryInterface() + { + return new MemoryQueryAction(); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public InsertInterface getInsertInterface() + { + return (new MemoryInsertAction()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public UpdateInterface getUpdateInterface() + { + return (new MemoryUpdateAction()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public DeleteInterface getDeleteInterface() + { + return (new MemoryDeleteAction()); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryCountAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryCountAction.java new file mode 100644 index 00000000..4f5e0d67 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryCountAction.java @@ -0,0 +1,54 @@ +/* + * 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.modules.backend.implementations.memory; + + +import com.kingsrook.qqq.backend.core.actions.interfaces.CountInterface; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput; + + +/******************************************************************************* + ** In-memory version of count action. + ** + *******************************************************************************/ +public class MemoryCountAction implements CountInterface +{ + + /******************************************************************************* + ** + *******************************************************************************/ + public CountOutput execute(CountInput countInput) throws QException + { + try + { + CountOutput countOutput = new CountOutput(); + countOutput.setCount(MemoryRecordStore.getInstance().count(countInput)); + return (countOutput); + } + catch(Exception e) + { + throw new QException("Error executing count", e); + } + } +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryDeleteAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryDeleteAction.java new file mode 100644 index 00000000..50c5e239 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryDeleteAction.java @@ -0,0 +1,55 @@ +/* + * 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.modules.backend.implementations.memory; + + +import com.kingsrook.qqq.backend.core.actions.interfaces.DeleteInterface; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteOutput; + + +/******************************************************************************* + ** In-memory version of delete action. + ** + *******************************************************************************/ +public class MemoryDeleteAction implements DeleteInterface +{ + + /******************************************************************************* + ** + *******************************************************************************/ + public DeleteOutput execute(DeleteInput deleteInput) throws QException + { + try + { + DeleteOutput deleteOutput = new DeleteOutput(); + deleteOutput.setDeletedRecordCount(MemoryRecordStore.getInstance().delete(deleteInput)); + return (deleteOutput); + } + catch(Exception e) + { + throw new QException("Error executing delete: " + e.getMessage(), e); + } + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryInsertAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryInsertAction.java new file mode 100644 index 00000000..01839359 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryInsertAction.java @@ -0,0 +1,55 @@ +/* + * 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.modules.backend.implementations.memory; + + +import com.kingsrook.qqq.backend.core.actions.interfaces.InsertInterface; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput; + + +/******************************************************************************* + ** In-memory version of insert action. + ** + *******************************************************************************/ +public class MemoryInsertAction implements InsertInterface +{ + + /******************************************************************************* + ** + *******************************************************************************/ + public InsertOutput execute(InsertInput insertInput) throws QException + { + try + { + InsertOutput insertOutput = new InsertOutput(); + insertOutput.setRecords(MemoryRecordStore.getInstance().insert(insertInput, true)); + return (insertOutput); + } + catch(Exception e) + { + throw new QException("Error executing insert: " + e.getMessage(), e); + } + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryQueryAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryQueryAction.java new file mode 100644 index 00000000..cd0e8bf7 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryQueryAction.java @@ -0,0 +1,55 @@ +/* + * 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.modules.backend.implementations.memory; + + +import com.kingsrook.qqq.backend.core.actions.interfaces.QueryInterface; +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.tables.query.QueryOutput; + + +/******************************************************************************* + ** In-memory version of query action. + ** + *******************************************************************************/ +public class MemoryQueryAction implements QueryInterface +{ + + /******************************************************************************* + ** + *******************************************************************************/ + public QueryOutput execute(QueryInput queryInput) throws QException + { + try + { + QueryOutput queryOutput = new QueryOutput(queryInput); + queryOutput.addRecords(MemoryRecordStore.getInstance().query(queryInput)); + return (queryOutput); + } + catch(Exception e) + { + throw new QException("Error executing query", e); + } + } + +} 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 new file mode 100644 index 00000000..90eedd28 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java @@ -0,0 +1,360 @@ +/* + * 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.modules.backend.implementations.memory; + + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput; +import com.kingsrook.qqq.backend.core.model.data.QRecord; +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.tables.QTableMetaData; +import org.apache.commons.lang.NotImplementedException; + + +/******************************************************************************* + ** Storage provider for the MemoryBackendModule + *******************************************************************************/ +public class MemoryRecordStore +{ + private static MemoryRecordStore instance; + + private Map> data; + private Map nextSerials; + + private static boolean collectStatistics = false; + + private static final Map statistics = Collections.synchronizedMap(new HashMap<>()); + + public static final String STAT_QUERIES_RAN = "queriesRan"; + + + + /******************************************************************************* + ** private singleton constructor + *******************************************************************************/ + private MemoryRecordStore() + { + data = new HashMap<>(); + nextSerials = new HashMap<>(); + } + + + + /******************************************************************************* + ** Forget all data in the memory store... + *******************************************************************************/ + public void reset() + { + data.clear(); + nextSerials.clear(); + } + + + + /******************************************************************************* + ** singleton accessor + *******************************************************************************/ + public static MemoryRecordStore getInstance() + { + if(instance == null) + { + instance = new MemoryRecordStore(); + } + return (instance); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private Map getTableData(QTableMetaData table) + { + if(!data.containsKey(table.getName())) + { + data.put(table.getName(), new HashMap<>()); + } + return (data.get(table.getName())); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public List query(QueryInput input) + { + incrementStatistic(STAT_QUERIES_RAN); + + Map tableData = getTableData(input.getTable()); + List records = new ArrayList<>(); + + for(QRecord qRecord : tableData.values()) + { + boolean recordMatches = true; + if(input.getFilter() != null && input.getFilter().getCriteria() != null) + { + for(QFilterCriteria criterion : input.getFilter().getCriteria()) + { + String fieldName = criterion.getFieldName(); + Serializable value = qRecord.getValue(fieldName); + switch(criterion.getOperator()) + { + case EQUALS: + { + if(!value.equals(criterion.getValues().get(0))) + { + recordMatches = false; + } + break; + } + case IN: + { + if(!criterion.getValues().contains(value)) + { + recordMatches = false; + } + break; + } + default: + { + throw new NotImplementedException("Operator [" + criterion.getOperator() + "] is not yet implemented in the Memory backend."); + } + } + if(!recordMatches) + { + break; + } + } + } + + if(recordMatches) + { + records.add(qRecord); + } + } + + return (records); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public Integer count(CountInput input) + { + QueryInput queryInput = new QueryInput(input.getInstance()); + queryInput.setSession(input.getSession()); + queryInput.setTableName(input.getTableName()); + queryInput.setFilter(input.getFilter()); + List queryResult = query(queryInput); + + return (queryResult.size()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public List insert(InsertInput input, boolean returnInsertedRecords) + { + if(input.getRecords() == null) + { + return (new ArrayList<>()); + } + + QTableMetaData table = input.getTable(); + Map tableData = getTableData(table); + + //////////////////////////////////////// + // grab the next unique serial to use // + //////////////////////////////////////// + Integer nextSerial = nextSerials.get(table.getName()); + if(nextSerial == null) + { + nextSerial = 1; + } + + while(tableData.containsKey(nextSerial)) + { + nextSerial++; + } + + List outputRecords = new ArrayList<>(); + QFieldMetaData primaryKeyField = table.getField(table.getPrimaryKeyField()); + for(QRecord record : input.getRecords()) + { + ///////////////////////////////////////////////// + // set the next serial in the record if needed // + ///////////////////////////////////////////////// + if(record.getValue(primaryKeyField.getName()) == null && primaryKeyField.getType().equals(QFieldType.INTEGER)) + { + record.setValue(primaryKeyField.getName(), nextSerial++); + } + + /////////////////////////////////////////////////////////////////////////////////////////////////// + // make sure that if the user supplied a serial, greater than the one we had, that we skip ahead // + /////////////////////////////////////////////////////////////////////////////////////////////////// + if(primaryKeyField.getType().equals(QFieldType.INTEGER) && record.getValueInteger(primaryKeyField.getName()) > nextSerial) + { + nextSerial = record.getValueInteger(primaryKeyField.getName()) + 1; + } + + tableData.put(record.getValue(primaryKeyField.getName()), record); + if(returnInsertedRecords) + { + outputRecords.add(record); + } + } + + nextSerials.put(table.getName(), nextSerial); + + return (outputRecords); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public List update(UpdateInput input, boolean returnUpdatedRecords) + { + if(input.getRecords() == null) + { + return (new ArrayList<>()); + } + + QTableMetaData table = input.getTable(); + Map tableData = getTableData(table); + + List outputRecords = new ArrayList<>(); + QFieldMetaData primaryKeyField = table.getField(table.getPrimaryKeyField()); + for(QRecord record : input.getRecords()) + { + Serializable primaryKeyValue = record.getValue(primaryKeyField.getName()); + if(tableData.containsKey(primaryKeyValue)) + { + QRecord recordToUpdate = tableData.get(primaryKeyValue); + for(Map.Entry valueEntry : record.getValues().entrySet()) + { + recordToUpdate.setValue(valueEntry.getKey(), valueEntry.getValue()); + } + + if(returnUpdatedRecords) + { + outputRecords.add(record); + } + } + } + + return (outputRecords); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public int delete(DeleteInput input) + { + if(input.getPrimaryKeys() == null) + { + return (0); + } + + QTableMetaData table = input.getTable(); + Map tableData = getTableData(table); + int rowsDeleted = 0; + for(Serializable primaryKeyValue : input.getPrimaryKeys()) + { + if(tableData.containsKey(primaryKeyValue)) + { + tableData.remove(primaryKeyValue); + rowsDeleted++; + } + } + + return (rowsDeleted); + } + + + + /******************************************************************************* + ** Setter for collectStatistics + ** + *******************************************************************************/ + public static void setCollectStatistics(boolean collectStatistics) + { + MemoryRecordStore.collectStatistics = collectStatistics; + } + + + + /******************************************************************************* + ** Increment a statistic + ** + *******************************************************************************/ + public static void incrementStatistic(String statName) + { + if(collectStatistics) + { + statistics.putIfAbsent(statName, 0); + statistics.put(statName, statistics.get(statName) + 1); + } + } + + + + /******************************************************************************* + ** clear the map of statistics + ** + *******************************************************************************/ + public static void resetStatistics() + { + statistics.clear(); + } + + + + /******************************************************************************* + ** Getter for statistics + ** + *******************************************************************************/ + public static Map getStatistics() + { + return statistics; + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryUpdateAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryUpdateAction.java new file mode 100644 index 00000000..97793ce6 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryUpdateAction.java @@ -0,0 +1,55 @@ +/* + * 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.modules.backend.implementations.memory; + + +import com.kingsrook.qqq.backend.core.actions.interfaces.UpdateInterface; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateOutput; + + +/******************************************************************************* + ** In-memory version of update action. + ** + *******************************************************************************/ +public class MemoryUpdateAction implements UpdateInterface +{ + + /******************************************************************************* + ** + *******************************************************************************/ + public UpdateOutput execute(UpdateInput updateInput) throws QException + { + try + { + UpdateOutput updateOutput = new UpdateOutput(); + updateOutput.setRecords(MemoryRecordStore.getInstance().update(updateInput, true)); + return (updateOutput); + } + catch(Exception e) + { + throw new QException("Error executing update: " + e.getMessage(), e); + } + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/basic/BasicETLLoadAsUpdateFunction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/basic/BasicETLLoadAsUpdateFunction.java index e6a8290f..0219acf6 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/basic/BasicETLLoadAsUpdateFunction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/basic/BasicETLLoadAsUpdateFunction.java @@ -82,6 +82,8 @@ public class BasicETLLoadAsUpdateFunction implements BackendStep for(List page : CollectionUtils.getPages(inputRecords, pageSize)) { LOG.info("Updating a page of [" + page.size() + "] records. Progress: " + recordsUpdated + " loaded out of " + inputRecords.size() + " total"); + runBackendStepInput.getAsyncJobCallback().updateStatus("Updating records", recordsUpdated, inputRecords.size()); + UpdateInput updateInput = new UpdateInput(runBackendStepInput.getInstance()); updateInput.setSession(runBackendStepInput.getSession()); updateInput.setTableName(table); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/basic/BasicETLLoadFunction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/basic/BasicETLLoadFunction.java index f8e782f2..d0a6c77b 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/basic/BasicETLLoadFunction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/basic/BasicETLLoadFunction.java @@ -86,6 +86,8 @@ public class BasicETLLoadFunction implements BackendStep for(List page : CollectionUtils.getPages(inputRecords, pageSize)) { LOG.info("Inserting a page of [" + page.size() + "] records. Progress: " + recordsInserted + " loaded out of " + inputRecords.size() + " total"); + runBackendStepInput.getAsyncJobCallback().updateStatus("Inserting records", recordsInserted, inputRecords.size()); + InsertInput insertInput = new InsertInput(runBackendStepInput.getInstance()); insertInput.setSession(runBackendStepInput.getSession()); insertInput.setTableName(table); 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 08ecf054..4d422746 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 @@ -44,6 +44,7 @@ public class ValueUtils { private static final DateTimeFormatter dateTimeFormatter_yyyyMMddWithDashes = DateTimeFormatter.ofPattern("yyyy-MM-dd"); private static final DateTimeFormatter dateTimeFormatter_MdyyyyWithSlashes = DateTimeFormatter.ofPattern("M/d/yyyy"); + private static final DateTimeFormatter dateTimeFormatter_yyyyMMdd = DateTimeFormatter.ofPattern("yyyyMMdd"); @@ -262,7 +263,7 @@ public class ValueUtils private static LocalDate tryLocalDateParsers(String s) { DateTimeParseException lastException = null; - for(DateTimeFormatter dateTimeFormatter : List.of(dateTimeFormatter_yyyyMMddWithDashes, dateTimeFormatter_MdyyyyWithSlashes)) + for(DateTimeFormatter dateTimeFormatter : List.of(dateTimeFormatter_yyyyMMddWithDashes, dateTimeFormatter_MdyyyyWithSlashes, dateTimeFormatter_yyyyMMdd)) { try { @@ -422,7 +423,7 @@ public class ValueUtils /******************************************************************************* ** *******************************************************************************/ - public static Object getValueAsLocalTime(Serializable value) + public static LocalTime getValueAsLocalTime(Serializable value) { try { diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/QueryActionTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/QueryActionTest.java index 326e0742..a936e6b4 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/QueryActionTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/QueryActionTest.java @@ -22,6 +22,8 @@ package com.kingsrook.qqq.backend.core.actions.tables; +import java.util.List; +import com.kingsrook.qqq.backend.core.actions.reporting.RecordPipe; 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.tables.query.QueryOutput; @@ -47,18 +49,79 @@ class QueryActionTest @Test public void test() throws QException { - QueryInput request = new QueryInput(TestUtils.defineInstance()); - request.setSession(TestUtils.getMockSession()); - request.setTableName("person"); - QueryOutput result = new QueryAction().execute(request); - assertNotNull(result); + QueryInput queryInput = new QueryInput(TestUtils.defineInstance()); + queryInput.setSession(TestUtils.getMockSession()); + queryInput.setTableName("person"); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + assertNotNull(queryOutput); - assertThat(result.getRecords()).isNotEmpty(); - for(QRecord record : result.getRecords()) + assertThat(queryOutput.getRecords()).isNotEmpty(); + for(QRecord record : queryOutput.getRecords()) { assertThat(record.getValues()).isNotEmpty(); - assertThat(record.getDisplayValues()).isNotEmpty(); assertThat(record.getErrors()).isEmpty(); + + /////////////////////////////////////////////////////////////// + // this SHOULD be empty, based on the default for the should // + /////////////////////////////////////////////////////////////// + assertThat(record.getDisplayValues()).isEmpty(); + } + + //////////////////////////////////// + // now flip that field and re-run // + //////////////////////////////////// + queryInput.setShouldGenerateDisplayValues(true); + assertThat(queryOutput.getRecords()).isNotEmpty(); + queryOutput = new QueryAction().execute(queryInput); + for(QRecord record : queryOutput.getRecords()) + { + assertThat(record.getDisplayValues()).isNotEmpty(); } } + + + + /******************************************************************************* + ** Test running with a recordPipe - using the shape table, which uses the memory + ** backend, which is known to do an addAll to the query output. + ** + *******************************************************************************/ + @Test + public void testRecordPipeShapeTable() throws QException + { + TestUtils.insertDefaultShapes(TestUtils.defineInstance()); + + RecordPipe pipe = new RecordPipe(); + QueryInput queryInput = new QueryInput(TestUtils.defineInstance()); + queryInput.setSession(TestUtils.getMockSession()); + queryInput.setTableName(TestUtils.TABLE_NAME_SHAPE); + queryInput.setRecordPipe(pipe); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + assertNotNull(queryOutput); + + List records = pipe.consumeAvailableRecords(); + assertThat(records).isNotEmpty(); + } + + + /******************************************************************************* + ** Test running with a recordPipe - using the person table, which uses the mock + ** backend, which is known to do a single-add (not addAll) to the query output. + ** + *******************************************************************************/ + @Test + public void testRecordPipePersonTable() throws QException + { + RecordPipe pipe = new RecordPipe(); + QueryInput queryInput = new QueryInput(TestUtils.defineInstance()); + queryInput.setSession(TestUtils.getMockSession()); + queryInput.setTableName(TestUtils.TABLE_NAME_PERSON); + queryInput.setRecordPipe(pipe); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + assertNotNull(queryOutput); + + List records = pipe.consumeAvailableRecords(); + assertThat(records).isNotEmpty(); + } + } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/values/QPossibleValueTranslatorTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/values/QPossibleValueTranslatorTest.java new file mode 100644 index 00000000..fd79e473 --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/values/QPossibleValueTranslatorTest.java @@ -0,0 +1,335 @@ +/* + * 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.math.BigDecimal; +import java.util.Collections; +import java.util.List; +import com.kingsrook.qqq.backend.core.exceptions.QException; +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.fields.QFieldType; +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.tables.QTableMetaData; +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.BeforeEach; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + + +/******************************************************************************* + ** Unit test for QPossibleValueTranslator + *******************************************************************************/ +public class QPossibleValueTranslatorTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @BeforeEach + void beforeEach() + { + MemoryRecordStore.getInstance().reset(); + MemoryRecordStore.resetStatistics(); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testPossibleValueEnum() + { + QInstance qInstance = TestUtils.defineInstance(); + QPossibleValueTranslator possibleValueTranslator = new QPossibleValueTranslator(qInstance, new QSession()); + QFieldMetaData stateField = qInstance.getTable("person").getField("homeStateId"); + QPossibleValueSource possibleValueSource = qInstance.getPossibleValueSource(stateField.getPossibleValueSourceName()); + + ////////////////////////////////////////////////////////////////////////// + // assert the default formatting for a not-found value is a null string // + ////////////////////////////////////////////////////////////////////////// + assertNull(possibleValueTranslator.translatePossibleValue(stateField, null)); + assertNull(possibleValueTranslator.translatePossibleValue(stateField, -1)); + + ////////////////////////////////////////////////////////////////////// + // let the not-found value be a simple string (no formatted values) // + ////////////////////////////////////////////////////////////////////// + possibleValueSource.setValueFormatIfNotFound("?"); + assertEquals("?", possibleValueTranslator.translatePossibleValue(stateField, null)); + assertEquals("?", possibleValueTranslator.translatePossibleValue(stateField, -1)); + + ///////////////////////////////////////////////////////////// + // let the not-found value be a string w/ formatted values // + ///////////////////////////////////////////////////////////// + possibleValueSource.setValueFormatIfNotFound("? (%s)"); + possibleValueSource.setValueFieldsIfNotFound(List.of("id")); + assertEquals("? ()", possibleValueTranslator.translatePossibleValue(stateField, null)); + assertEquals("? (-1)", possibleValueTranslator.translatePossibleValue(stateField, -1)); + + ///////////////////////////////////////////////////// + // assert the default formatting is just the label // + ///////////////////////////////////////////////////// + assertEquals("MO", possibleValueTranslator.translatePossibleValue(stateField, 2)); + assertEquals("IL", possibleValueTranslator.translatePossibleValue(stateField, 1)); + + ///////////////////////////////////////////////////////////////// + // assert the LABEL_ONLY format (when called out specifically) // + ///////////////////////////////////////////////////////////////// + possibleValueSource.setValueFormatAndFields(PVSValueFormatAndFields.LABEL_ONLY); + assertEquals("IL", possibleValueTranslator.translatePossibleValue(stateField, 1)); + + /////////////////////////////////////// + // assert the LABEL_PARAMS_ID format // + /////////////////////////////////////// + possibleValueSource.setValueFormatAndFields(PVSValueFormatAndFields.LABEL_PARENS_ID); + assertEquals("IL (1)", possibleValueTranslator.translatePossibleValue(stateField, 1)); + + ////////////////////////////////////// + // assert the ID_COLON_LABEL format // + ////////////////////////////////////// + possibleValueSource.setValueFormat(PVSValueFormatAndFields.ID_COLON_LABEL.getFormat()); + possibleValueSource.setValueFields(PVSValueFormatAndFields.ID_COLON_LABEL.getFields()); + assertEquals("1: IL", possibleValueTranslator.translatePossibleValue(stateField, 1)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testPossibleValueTable() throws QException + { + QInstance qInstance = TestUtils.defineInstance(); + QPossibleValueTranslator possibleValueTranslator = new QPossibleValueTranslator(qInstance, new QSession()); + QTableMetaData shapeTable = qInstance.getTable(TestUtils.TABLE_NAME_SHAPE); + QFieldMetaData shapeField = qInstance.getTable(TestUtils.TABLE_NAME_PERSON).getField("favoriteShapeId"); + QPossibleValueSource possibleValueSource = qInstance.getPossibleValueSource(shapeField.getPossibleValueSourceName()); + + TestUtils.insertDefaultShapes(qInstance); + + ////////////////////////////////////////////////////////////////////////// + // assert the default formatting for a not-found value is a null string // + ////////////////////////////////////////////////////////////////////////// + assertNull(possibleValueTranslator.translatePossibleValue(shapeField, null)); + assertNull(possibleValueTranslator.translatePossibleValue(shapeField, -1)); + + ////////////////////////////////////////////////////////////////////// + // let the not-found value be a simple string (no formatted values) // + ////////////////////////////////////////////////////////////////////// + possibleValueSource.setValueFormatIfNotFound("?"); + assertEquals("?", possibleValueTranslator.translatePossibleValue(shapeField, null)); + assertEquals("?", possibleValueTranslator.translatePossibleValue(shapeField, -1)); + + ///////////////////////////////////////////////////// + // assert the default formatting is just the label // + ///////////////////////////////////////////////////// + assertEquals("Square", possibleValueTranslator.translatePossibleValue(shapeField, 2)); + assertEquals("Triangle", possibleValueTranslator.translatePossibleValue(shapeField, 1)); + + /////////////////////////////////////// + // assert the LABEL_PARAMS_ID format // + /////////////////////////////////////// + possibleValueSource.setValueFormatAndFields(PVSValueFormatAndFields.LABEL_PARENS_ID); + assertEquals("Circle (3)", possibleValueTranslator.translatePossibleValue(shapeField, 3)); + + /////////////////////////////////////////////////////////// + // assert that we don't re-run queries for cached values // + /////////////////////////////////////////////////////////// + possibleValueTranslator = new QPossibleValueTranslator(qInstance, new QSession()); + MemoryRecordStore.setCollectStatistics(true); + possibleValueTranslator.translatePossibleValue(shapeField, 1); + possibleValueTranslator.translatePossibleValue(shapeField, 2); + assertEquals(2, MemoryRecordStore.getStatistics().get(MemoryRecordStore.STAT_QUERIES_RAN), "Should have ran 2 queries so far"); + possibleValueTranslator.translatePossibleValue(shapeField, 2); + possibleValueTranslator.translatePossibleValue(shapeField, 3); + possibleValueTranslator.translatePossibleValue(shapeField, 3); + assertEquals(3, MemoryRecordStore.getStatistics().get(MemoryRecordStore.STAT_QUERIES_RAN), "Should have ran 3 queries in total"); + + /////////////////////////////////////////////////////////////// + // assert that if we prime the cache, we can do just 1 query // + /////////////////////////////////////////////////////////////// + possibleValueTranslator = new QPossibleValueTranslator(qInstance, new QSession()); + List personRecords = List.of( + new QRecord().withTableName(TestUtils.TABLE_NAME_PERSON).withValue("favoriteShapeId", 1), + new QRecord().withTableName(TestUtils.TABLE_NAME_PERSON).withValue("favoriteShapeId", 1), + new QRecord().withTableName(TestUtils.TABLE_NAME_PERSON).withValue("favoriteShapeId", 2), + new QRecord().withTableName(TestUtils.TABLE_NAME_PERSON).withValue("favoriteShapeId", 2), + new QRecord().withTableName(TestUtils.TABLE_NAME_PERSON).withValue("favoriteShapeId", 3) + ); + QTableMetaData personTable = qInstance.getTable(TestUtils.TABLE_NAME_PERSON); + MemoryRecordStore.resetStatistics(); + possibleValueTranslator.primePvsCache(personTable, personRecords); + assertEquals(1, MemoryRecordStore.getStatistics().get(MemoryRecordStore.STAT_QUERIES_RAN), "Should only run 1 query"); + possibleValueTranslator.translatePossibleValue(shapeField, 1); + possibleValueTranslator.translatePossibleValue(shapeField, 2); + assertEquals(1, MemoryRecordStore.getStatistics().get(MemoryRecordStore.STAT_QUERIES_RAN), "Should only run 1 query"); + } + + + + /******************************************************************************* + ** Make sure that if we have 2 different PVS's pointed at the same 1 table, + ** that we avoid re-doing queries, and that we actually get different (formatted) values. + *******************************************************************************/ + @Test + void testPossibleValueTableMultiplePvsForATable() throws QException + { + QInstance qInstance = TestUtils.defineInstance(); + QTableMetaData shapeTable = qInstance.getTable(TestUtils.TABLE_NAME_SHAPE); + QTableMetaData personTable = qInstance.getTable(TestUtils.TABLE_NAME_PERSON); + + //////////////////////////////////////////////////////////////////// + // define a second version of the Shape PVS, with a unique format // + //////////////////////////////////////////////////////////////////// + qInstance.addPossibleValueSource(new QPossibleValueSource() + .withName("shapeV2") + .withType(QPossibleValueSourceType.TABLE) + .withTableName(TestUtils.TABLE_NAME_SHAPE) + .withValueFormat("%d: %s") + .withValueFields(List.of("id", "label")) + ); + + ////////////////////////////////////////////////////// + // use that PVS in a new column on the person table // + ////////////////////////////////////////////////////// + personTable.addField(new QFieldMetaData("currentShapeId", QFieldType.INTEGER) + .withPossibleValueSourceName("shapeV2") + ); + + TestUtils.insertDefaultShapes(qInstance); + + /////////////////////////////////////////////////////// + // define a list of persons pointing at those shapes // + /////////////////////////////////////////////////////// + List personRecords = List.of( + new QRecord().withTableName(TestUtils.TABLE_NAME_PERSON).withValue("favoriteShapeId", 1).withValue("currentShapeId", 2), + new QRecord().withTableName(TestUtils.TABLE_NAME_PERSON).withValue("favoriteShapeId", 1).withValue("currentShapeId", 3), + new QRecord().withTableName(TestUtils.TABLE_NAME_PERSON).withValue("favoriteShapeId", 2).withValue("currentShapeId", 3), + new QRecord().withTableName(TestUtils.TABLE_NAME_PERSON).withValue("favoriteShapeId", 2).withValue("currentShapeId", 3) + ); + + ///////////////////////// + // translate the PVS's // + ///////////////////////// + MemoryRecordStore.setCollectStatistics(true); + new QPossibleValueTranslator(qInstance, new QSession()).translatePossibleValuesInRecords(personTable, personRecords); + + ///////////////////////////////// + // assert only 1 query was ran // + ///////////////////////////////// + assertEquals(1, MemoryRecordStore.getStatistics().get(MemoryRecordStore.STAT_QUERIES_RAN), "Should only run 1 query"); + + //////////////////////////////////////// + // assert expected values and formats // + //////////////////////////////////////// + assertEquals("Triangle", personRecords.get(0).getDisplayValue("favoriteShapeId")); + assertEquals("2: Square", personRecords.get(0).getDisplayValue("currentShapeId")); + assertEquals("Triangle", personRecords.get(1).getDisplayValue("favoriteShapeId")); + assertEquals("3: Circle", personRecords.get(1).getDisplayValue("currentShapeId")); + assertEquals("Square", personRecords.get(2).getDisplayValue("favoriteShapeId")); + assertEquals("3: Circle", personRecords.get(2).getDisplayValue("currentShapeId")); + } + + + + /******************************************************************************* + ** Make sure that if we have 2 different PVS's pointed at the same 1 table, + ** that we avoid re-doing queries, and that we actually get different (formatted) values. + *******************************************************************************/ + @Test + void testCustomPossibleValue() throws QException + { + QInstance qInstance = TestUtils.defineInstance(); + QTableMetaData personTable = qInstance.getTable(TestUtils.TABLE_NAME_PERSON); + String fieldName = "customValue"; + + ////////////////////////////////////////////////////////////// + // define a list of persons with values in the custom field // + ////////////////////////////////////////////////////////////// + List personRecords = List.of( + new QRecord().withTableName(TestUtils.TABLE_NAME_PERSON).withValue(fieldName, 1), + new QRecord().withTableName(TestUtils.TABLE_NAME_PERSON).withValue(fieldName, 2), + new QRecord().withTableName(TestUtils.TABLE_NAME_PERSON).withValue(fieldName, "Buckle my shoe") + ); + + ///////////////////////// + // translate the PVS's // + ///////////////////////// + new QPossibleValueTranslator(qInstance, new QSession()).translatePossibleValuesInRecords(personTable, personRecords); + + //////////////////////////////////////// + // assert expected values and formats // + //////////////////////////////////////// + assertEquals("Custom[1]", personRecords.get(0).getDisplayValue(fieldName)); + assertEquals("Custom[2]", personRecords.get(1).getDisplayValue(fieldName)); + assertEquals("Custom[Buckle my shoe]", personRecords.get(2).getDisplayValue(fieldName)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testSetDisplayValuesInRecords() + { + QTableMetaData table = TestUtils.defineTablePerson(); + + ///////////////////////////////////////////////////////////////// + // first, make sure it doesn't crash with null or empty inputs // + ///////////////////////////////////////////////////////////////// + QPossibleValueTranslator possibleValueTranslator = new QPossibleValueTranslator(TestUtils.defineInstance(), new QSession()); + possibleValueTranslator.translatePossibleValuesInRecords(table, null); + possibleValueTranslator.translatePossibleValuesInRecords(table, Collections.emptyList()); + + List records = List.of( + new QRecord() + .withValue("firstName", "Tim") + .withValue("lastName", "Chamberlain") + .withValue("price", new BigDecimal("3.50")) + .withValue("homeStateId", 1), + new QRecord() + .withValue("firstName", "Tyler") + .withValue("lastName", "Samples") + .withValue("price", new BigDecimal("174999.99")) + .withValue("homeStateId", 2) + ); + + possibleValueTranslator.translatePossibleValuesInRecords(table, records); + + assertNull(records.get(0).getRecordLabel()); // regular display stuff NOT done by PVS translator + assertNull(records.get(0).getDisplayValue("price")); + + assertEquals("IL", records.get(0).getDisplayValue("homeStateId")); + assertEquals("MO", records.get(1).getDisplayValue("homeStateId")); + } + +} diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/values/QValueFormatterTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/values/QValueFormatterTest.java index 4be9cf52..27428209 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/values/QValueFormatterTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/values/QValueFormatterTest.java @@ -30,6 +30,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.DisplayFormat; 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.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.utils.TestUtils; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; @@ -47,24 +48,26 @@ class QValueFormatterTest @Test void testFormatValue() { - assertNull(QValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.COMMAS), null)); + QValueFormatter qValueFormatter = new QValueFormatter(); - assertEquals("1", QValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.COMMAS), 1)); - assertEquals("1,000", QValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.COMMAS), 1000)); - assertEquals("1000", QValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(null), 1000)); - assertEquals("$1,000.00", QValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.CURRENCY), 1000)); - assertEquals("1,000.00", QValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.DECIMAL2_COMMAS), 1000)); - assertEquals("1000.00", QValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.DECIMAL2), 1000)); + assertNull(qValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.COMMAS), null)); - assertEquals("1", QValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.COMMAS), new BigDecimal("1"))); - assertEquals("1,000", QValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.COMMAS), new BigDecimal("1000"))); - assertEquals("1000", QValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.STRING), new BigDecimal("1000"))); - assertEquals("1000", QValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.STRING), 1000)); + assertEquals("1", qValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.COMMAS), 1)); + assertEquals("1,000", qValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.COMMAS), 1000)); + assertEquals("1000", qValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(null), 1000)); + assertEquals("$1,000.00", qValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.CURRENCY), 1000)); + assertEquals("1,000.00", qValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.DECIMAL2_COMMAS), 1000)); + assertEquals("1000.00", qValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.DECIMAL2), 1000)); + + assertEquals("1", qValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.COMMAS), new BigDecimal("1"))); + assertEquals("1,000", qValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.COMMAS), new BigDecimal("1000"))); + assertEquals("1000", qValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.STRING), new BigDecimal("1000"))); + assertEquals("1000", qValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.STRING), 1000)); ////////////////////////////////////////////////// // this one flows through the exceptional cases // ////////////////////////////////////////////////// - assertEquals("1000.01", QValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.COMMAS), new BigDecimal("1000.01"))); + assertEquals("1000.01", qValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.COMMAS), new BigDecimal("1000.01"))); } @@ -75,40 +78,42 @@ class QValueFormatterTest @Test void testFormatRecordLabel() { - QTableMetaData table = new QTableMetaData().withRecordLabelFormat("%s %s").withRecordLabelFields(List.of("firstName", "lastName")); - assertEquals("Darin Kelkhoff", QValueFormatter.formatRecordLabel(table, new QRecord().withValue("firstName", "Darin").withValue("lastName", "Kelkhoff"))); - assertEquals("Darin ", QValueFormatter.formatRecordLabel(table, new QRecord().withValue("firstName", "Darin"))); - assertEquals("Darin ", QValueFormatter.formatRecordLabel(table, new QRecord().withValue("firstName", "Darin").withValue("lastName", null))); + QValueFormatter qValueFormatter = new QValueFormatter(); - table = new QTableMetaData().withRecordLabelFormat("%s " + DisplayFormat.CURRENCY).withRecordLabelFields(List.of("firstName", "price")); - assertEquals("Darin $10,000.00", QValueFormatter.formatRecordLabel(table, new QRecord().withValue("firstName", "Darin").withValue("price", new BigDecimal(10000)))); + QTableMetaData table = new QTableMetaData().withRecordLabelFormat("%s %s").withRecordLabelFields(List.of("firstName", "lastName")); + assertEquals("Darin Kelkhoff", qValueFormatter.formatRecordLabel(table, new QRecord().withValue("firstName", "Darin").withValue("lastName", "Kelkhoff"))); + assertEquals("Darin ", qValueFormatter.formatRecordLabel(table, new QRecord().withValue("firstName", "Darin"))); + assertEquals("Darin ", qValueFormatter.formatRecordLabel(table, new QRecord().withValue("firstName", "Darin").withValue("lastName", null))); + + table = new QTableMetaData().withRecordLabelFormat("%s " + DisplayFormat.CURRENCY).withRecordLabelFields("firstName", "price"); + assertEquals("Darin $10,000.00", qValueFormatter.formatRecordLabel(table, new QRecord().withValue("firstName", "Darin").withValue("price", new BigDecimal(10000)))); table = new QTableMetaData().withRecordLabelFormat(DisplayFormat.DEFAULT).withRecordLabelFields(List.of("id")); - assertEquals("123456", QValueFormatter.formatRecordLabel(table, new QRecord().withValue("id", "123456"))); + assertEquals("123456", qValueFormatter.formatRecordLabel(table, new QRecord().withValue("id", "123456"))); /////////////////////////////////////////////////////// // exceptional flow: no recordLabelFormat specified // /////////////////////////////////////////////////////// table = new QTableMetaData().withPrimaryKeyField("id"); - assertEquals("42", QValueFormatter.formatRecordLabel(table, new QRecord().withValue("id", 42))); + assertEquals("42", qValueFormatter.formatRecordLabel(table, new QRecord().withValue("id", 42))); ///////////////////////////////////////////////// // exceptional flow: no fields for the format // ///////////////////////////////////////////////// table = new QTableMetaData().withRecordLabelFormat("%s %s").withPrimaryKeyField("id"); - assertEquals("128", QValueFormatter.formatRecordLabel(table, new QRecord().withValue("id", 128))); + assertEquals("128", qValueFormatter.formatRecordLabel(table, new QRecord().withValue("id", 128))); ///////////////////////////////////////////////////////// // exceptional flow: not enough fields for the format // ///////////////////////////////////////////////////////// - table = new QTableMetaData().withRecordLabelFormat("%s %s").withRecordLabelFields(List.of("a")).withPrimaryKeyField("id"); - assertEquals("256", QValueFormatter.formatRecordLabel(table, new QRecord().withValue("a", 47).withValue("id", 256))); + table = new QTableMetaData().withRecordLabelFormat("%s %s").withRecordLabelFields("a").withPrimaryKeyField("id"); + assertEquals("256", qValueFormatter.formatRecordLabel(table, new QRecord().withValue("a", 47).withValue("id", 256))); ////////////////////////////////////////////////////////////////////////////////////////////////////////// // exceptional flow (kinda): too many fields for the format (just get the ones that are in the format) // ////////////////////////////////////////////////////////////////////////////////////////////////////////// table = new QTableMetaData().withRecordLabelFormat("%s %s").withRecordLabelFields(List.of("a", "b", "c")).withPrimaryKeyField("id"); - assertEquals("47 48", QValueFormatter.formatRecordLabel(table, new QRecord().withValue("a", 47).withValue("b", 48).withValue("c", 49).withValue("id", 256))); + assertEquals("47 48", qValueFormatter.formatRecordLabel(table, new QRecord().withValue("a", 47).withValue("b", 48).withValue("c", 49).withValue("id", 256))); } @@ -121,40 +126,46 @@ class QValueFormatterTest { QTableMetaData table = new QTableMetaData() .withRecordLabelFormat("%s %s") - .withRecordLabelFields(List.of("firstName", "lastName")) + .withRecordLabelFields("firstName", "lastName") .withField(new QFieldMetaData("firstName", QFieldType.STRING)) .withField(new QFieldMetaData("lastName", QFieldType.STRING)) .withField(new QFieldMetaData("price", QFieldType.DECIMAL).withDisplayFormat(DisplayFormat.CURRENCY)) - .withField(new QFieldMetaData("quantity", QFieldType.INTEGER).withDisplayFormat(DisplayFormat.COMMAS)); + .withField(new QFieldMetaData("quantity", QFieldType.INTEGER).withDisplayFormat(DisplayFormat.COMMAS)) + .withField(new QFieldMetaData("homeStateId", QFieldType.INTEGER).withPossibleValueSourceName(TestUtils.POSSIBLE_VALUE_SOURCE_STATE)); ///////////////////////////////////////////////////////////////// // first, make sure it doesn't crash with null or empty inputs // ///////////////////////////////////////////////////////////////// - QValueFormatter.setDisplayValuesInRecords(table, null); - QValueFormatter.setDisplayValuesInRecords(table, Collections.emptyList()); + QValueFormatter qValueFormatter = new QValueFormatter(); + qValueFormatter.setDisplayValuesInRecords(table, null); + qValueFormatter.setDisplayValuesInRecords(table, Collections.emptyList()); List records = List.of( new QRecord() .withValue("firstName", "Tim") .withValue("lastName", "Chamberlain") .withValue("price", new BigDecimal("3.50")) - .withValue("quantity", 1701), + .withValue("quantity", 1701) + .withValue("homeStateId", 1), new QRecord() .withValue("firstName", "Tyler") .withValue("lastName", "Samples") .withValue("price", new BigDecimal("174999.99")) .withValue("quantity", 47) + .withValue("homeStateId", 2) ); - QValueFormatter.setDisplayValuesInRecords(table, records); + qValueFormatter.setDisplayValuesInRecords(table, records); assertEquals("Tim Chamberlain", records.get(0).getRecordLabel()); assertEquals("$3.50", records.get(0).getDisplayValue("price")); assertEquals("1,701", records.get(0).getDisplayValue("quantity")); + assertEquals("1", records.get(0).getDisplayValue("homeStateId")); // PVS NOT translated by this class. assertEquals("Tyler Samples", records.get(1).getRecordLabel()); assertEquals("$174,999.99", records.get(1).getDisplayValue("price")); assertEquals("47", records.get(1).getDisplayValue("quantity")); + assertEquals("2", records.get(1).getDisplayValue("homeStateId")); // PVS NOT translated by this class. } } \ No newline at end of file diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/adapters/CsvToQRecordAdapterTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/adapters/CsvToQRecordAdapterTest.java index 2593b3e2..7dff6bad 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/adapters/CsvToQRecordAdapterTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/adapters/CsvToQRecordAdapterTest.java @@ -31,6 +31,7 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; @@ -281,4 +282,65 @@ class CsvToQRecordAdapterTest // todo - this is what the method header comment means when it says we don't handle all cases well... // Assertions.assertEquals(List.of("A", "B", "C", "C 2", "C 3"), csvToQRecordAdapter.makeHeadersUnique(List.of("A", "B", "C 2", "C", "C 3"))); } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testByteOrderMarker() + { + CsvToQRecordAdapter csvToQRecordAdapter = new CsvToQRecordAdapter(); + + ////////////////////////////////////////////////////////////////////////////////////////////////////////// + // note - there's a zero-width non-breaking-space character (0xFEFF or some-such) // + // at the start of this string!! You may not be able to see it, depending on where you view this file. // + ////////////////////////////////////////////////////////////////////////////////////////////////////////// + List records = csvToQRecordAdapter.buildRecordsFromCsv(""" + id,firstName + 1,John""", TestUtils.defineTablePerson(), null); + + assertEquals(1, records.get(0).getValueInteger("id")); + assertEquals("John", records.get(0).getValueString("firstName")); + } + + + + /******************************************************************************* + ** Fix an IndexOutOfBounds that we used to throw. + *******************************************************************************/ + @Test + void testTooFewBodyColumns() + { + CsvToQRecordAdapter csvToQRecordAdapter = new CsvToQRecordAdapter(); + List records = csvToQRecordAdapter.buildRecordsFromCsv(""" + id,firstName,lastName + 1,John""", TestUtils.defineTablePerson(), null); + + assertEquals(1, records.get(0).getValueInteger("id")); + assertEquals("John", records.get(0).getValueString("firstName")); + assertNull(records.get(0).getValueString("lastName")); + } + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + public void testTooFewColumnsIndexMapping() + { + int index = 1; + QIndexBasedFieldMapping mapping = new QIndexBasedFieldMapping() + .withMapping("id", index++) + .withMapping("firstName", index++) + .withMapping("lastName", index++); + + CsvToQRecordAdapter csvToQRecordAdapter = new CsvToQRecordAdapter(); + List records = csvToQRecordAdapter.buildRecordsFromCsv("1,John", TestUtils.defineTablePerson(), mapping); + + assertEquals(1, records.get(0).getValueInteger("id")); + assertEquals("John", records.get(0).getValueString("firstName")); + assertNull(records.get(0).getValueString("lastName")); + } + } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricherTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricherTest.java index f9017298..c86e9fca 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricherTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricherTest.java @@ -22,10 +22,10 @@ package com.kingsrook.qqq.backend.core.instances; +import java.util.ArrayList; import java.util.Collections; -import com.kingsrook.qqq.backend.core.exceptions.QException; -import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; 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.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.utils.TestUtils; @@ -130,6 +130,21 @@ class QInstanceEnricherTest + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testNameToLabel() + { + assertEquals("Address 2", QInstanceEnricher.nameToLabel("address2")); + assertEquals("Field 20", QInstanceEnricher.nameToLabel("field20")); + assertEquals("Something USA", QInstanceEnricher.nameToLabel("somethingUSA")); + assertEquals("Number 1 Dad", QInstanceEnricher.nameToLabel("number1Dad")); + assertEquals("Number 417 Dad", QInstanceEnricher.nameToLabel("number417Dad")); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -146,4 +161,28 @@ class QInstanceEnricherTest assertEquals("tla_and_another_tla", QInstanceEnricher.inferBackendName("TLAAndAnotherTLA")); } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testInferredRecordLabelFormat() + { + QInstance qInstance = TestUtils.defineInstance(); + QTableMetaData table = qInstance.getTable("person").withRecordLabelFormat(null).withRecordLabelFields(new ArrayList<>()); + new QInstanceEnricher().enrich(qInstance); + assertNull(table.getRecordLabelFormat()); + + qInstance = TestUtils.defineInstance(); + table = qInstance.getTable("person").withRecordLabelFormat(null).withRecordLabelFields("firstName"); + new QInstanceEnricher().enrich(qInstance); + assertEquals("%s", table.getRecordLabelFormat()); + + qInstance = TestUtils.defineInstance(); + table = qInstance.getTable("person").withRecordLabelFormat(null).withRecordLabelFields("firstName", "lastName"); + new QInstanceEnricher().enrich(qInstance); + assertEquals("%s %s", table.getRecordLabelFormat()); + } + } 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 a5cefc2c..ee2464a9 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 @@ -22,16 +22,25 @@ package com.kingsrook.qqq.backend.core.instances; +import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.function.Consumer; +import java.util.function.Function; +import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers; import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException; +import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeUsage; 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.layout.QAppMetaData; import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon; +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.tables.QFieldSection; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier; @@ -115,7 +124,8 @@ class QInstanceValidatorTest qInstance.setTables(null); qInstance.setProcesses(null); }, - "At least 1 table must be defined"); + "At least 1 table must be defined", + "Unrecognized table shape for possibleValueSource shape"); } @@ -132,7 +142,8 @@ class QInstanceValidatorTest qInstance.setTables(new HashMap<>()); qInstance.setProcesses(new HashMap<>()); }, - "At least 1 table must be defined"); + "At least 1 table must be defined", + "Unrecognized table shape for possibleValueSource shape"); } @@ -150,10 +161,13 @@ class QInstanceValidatorTest qInstance.getTable("person").setName("notPerson"); qInstance.getBackend("default").setName("notDefault"); qInstance.getProcess(TestUtils.PROCESS_NAME_GREET_PEOPLE).setName("notGreetPeople"); + qInstance.getPossibleValueSource(TestUtils.POSSIBLE_VALUE_SOURCE_STATE).setName("notStates"); }, "Inconsistent naming for table", "Inconsistent naming for backend", - "Inconsistent naming for process"); + "Inconsistent naming for process", + "Inconsistent naming for possibleValueSource" + ); } @@ -184,6 +198,18 @@ class QInstanceValidatorTest + /******************************************************************************* + ** + *******************************************************************************/ + @Test + public void test_validateTableBadRecordFormatField() + { + assertValidationFailureReasons((qInstance) -> qInstance.getTable("person").withRecordLabelFields("notAField"), + "not a field"); + } + + + /******************************************************************************* ** Test that if a process specifies a table that doesn't exist, that it fails. ** @@ -245,6 +271,138 @@ class QInstanceValidatorTest + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testTableCustomizers() + { + assertValidationFailureReasons((qInstance) -> qInstance.getTable("person").withCustomizer(TableCustomizers.POST_QUERY_RECORD.getRole(), new QCodeReference()), + "missing a code reference name", "missing a code type"); + + assertValidationFailureReasons((qInstance) -> qInstance.getTable("person").withCustomizer(TableCustomizers.POST_QUERY_RECORD.getRole(), new QCodeReference(null, QCodeType.JAVA, null)), + "missing a code reference name"); + + assertValidationFailureReasons((qInstance) -> qInstance.getTable("person").withCustomizer(TableCustomizers.POST_QUERY_RECORD.getRole(), new QCodeReference("", QCodeType.JAVA, null)), + "missing a code reference name"); + + assertValidationFailureReasons((qInstance) -> qInstance.getTable("person").withCustomizer(TableCustomizers.POST_QUERY_RECORD.getRole(), new QCodeReference("Test", null, null)), + "missing a code type"); + + assertValidationFailureReasons((qInstance) -> qInstance.getTable("person").withCustomizer(TableCustomizers.POST_QUERY_RECORD.getRole(), new QCodeReference("Test", QCodeType.JAVA, QCodeUsage.CUSTOMIZER)), + "Class for CodeReference could not be found"); + + assertValidationFailureReasons((qInstance) -> qInstance.getTable("person").withCustomizer(TableCustomizers.POST_QUERY_RECORD.getRole(), new QCodeReference(CustomizerWithNoVoidConstructor.class, QCodeUsage.CUSTOMIZER)), + "Instance of CodeReference could not be created"); + + assertValidationFailureReasons((qInstance) -> qInstance.getTable("person").withCustomizer(TableCustomizers.POST_QUERY_RECORD.getRole(), new QCodeReference(CustomizerThatIsNotAFunction.class, QCodeUsage.CUSTOMIZER)), + "CodeReference could not be casted"); + + assertValidationFailureReasons((qInstance) -> qInstance.getTable("person").withCustomizer(TableCustomizers.POST_QUERY_RECORD.getRole(), new QCodeReference(CustomizerFunctionWithIncorrectTypeParameters.class, QCodeUsage.CUSTOMIZER)), + "Error validating customizer type parameters"); + + assertValidationFailureReasons((qInstance) -> qInstance.getTable("person").withCustomizer(TableCustomizers.POST_QUERY_RECORD.getRole(), new QCodeReference(CustomizerFunctionWithIncorrectTypeParameter1.class, QCodeUsage.CUSTOMIZER)), + "Error validating customizer type parameters"); + + assertValidationFailureReasons((qInstance) -> qInstance.getTable("person").withCustomizer(TableCustomizers.POST_QUERY_RECORD.getRole(), new QCodeReference(CustomizerFunctionWithIncorrectTypeParameter2.class, QCodeUsage.CUSTOMIZER)), + "Error validating customizer type parameters"); + + assertValidationSuccess((qInstance) -> qInstance.getTable("person").withCustomizer(TableCustomizers.POST_QUERY_RECORD.getRole(), new QCodeReference(CustomizerValid.class, QCodeUsage.CUSTOMIZER))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static class CustomizerWithNoVoidConstructor + { + public CustomizerWithNoVoidConstructor(boolean b) + { + + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static class CustomizerWithOnlyPrivateConstructor + { + private CustomizerWithOnlyPrivateConstructor() + { + + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static class CustomizerThatIsNotAFunction + { + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static class CustomizerFunctionWithIncorrectTypeParameters implements Function + { + @Override + public String apply(String s) + { + return null; + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static class CustomizerFunctionWithIncorrectTypeParameter1 implements Function + { + @Override + public QRecord apply(String s) + { + return null; + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static class CustomizerFunctionWithIncorrectTypeParameter2 implements Function + { + @Override + public String apply(QRecord s) + { + return "Test"; + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static class CustomizerValid implements Function + { + @Override + public QRecord apply(QRecord record) + { + return null; + } + } + + + /******************************************************************************* ** Test that if a field specifies a backend that doesn't exist, that it fails. ** @@ -252,7 +410,7 @@ class QInstanceValidatorTest @Test public void test_validateFieldWithMissingPossibleValueSource() { - assertValidationFailureReasons((qInstance) -> qInstance.getTable("person").getField("homeState").setPossibleValueSourceName("not a real possible value source"), + assertValidationFailureReasons((qInstance) -> qInstance.getTable("person").getField("homeStateId").setPossibleValueSourceName("not a real possible value source"), "Unrecognized possibleValueSourceName"); } @@ -319,6 +477,7 @@ class QInstanceValidatorTest } + /******************************************************************************* ** *******************************************************************************/ @@ -376,6 +535,7 @@ class QInstanceValidatorTest } + /******************************************************************************* ** *******************************************************************************/ @@ -391,6 +551,7 @@ class QInstanceValidatorTest } + /******************************************************************************* ** *******************************************************************************/ @@ -408,6 +569,86 @@ class QInstanceValidatorTest + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testPossibleValueSourceMissingType() + { + assertValidationFailureReasons((qInstance) -> qInstance.getPossibleValueSource(TestUtils.POSSIBLE_VALUE_SOURCE_STATE).setType(null), + "Missing type for possibleValueSource"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testPossibleValueSourceMisConfiguredEnum() + { + assertValidationFailureReasons((qInstance) -> { + QPossibleValueSource possibleValueSource = qInstance.getPossibleValueSource(TestUtils.POSSIBLE_VALUE_SOURCE_STATE); + possibleValueSource.setTableName("person"); + possibleValueSource.setCustomCodeReference(new QCodeReference()); + possibleValueSource.setEnumValues(null); + }, + "should not have a tableName", + "should not have a customCodeReference", + "is missing enum values"); + + assertValidationFailureReasons((qInstance) -> qInstance.getPossibleValueSource(TestUtils.POSSIBLE_VALUE_SOURCE_STATE).setEnumValues(new ArrayList<>()), + "is missing enum values"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testPossibleValueSourceMisConfiguredTable() + { + assertValidationFailureReasons((qInstance) -> { + QPossibleValueSource possibleValueSource = qInstance.getPossibleValueSource(TestUtils.POSSIBLE_VALUE_SOURCE_SHAPE); + possibleValueSource.setTableName(null); + 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"); + + assertValidationFailureReasons((qInstance) -> qInstance.getPossibleValueSource(TestUtils.POSSIBLE_VALUE_SOURCE_SHAPE).setTableName("Not a table"), + "Unrecognized table"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testPossibleValueSourceMisConfiguredCustom() + { + assertValidationFailureReasons((qInstance) -> { + QPossibleValueSource possibleValueSource = qInstance.getPossibleValueSource(TestUtils.POSSIBLE_VALUE_SOURCE_CUSTOM); + possibleValueSource.setTableName("person"); + possibleValueSource.setCustomCodeReference(null); + possibleValueSource.setEnumValues(List.of(new QPossibleValue<>("test"))); + }, + "should not have enum values", + "should not have a tableName", + "is missing a customCodeReference"); + + assertValidationFailureReasons((qInstance) -> qInstance.getPossibleValueSource(TestUtils.POSSIBLE_VALUE_SOURCE_CUSTOM).setCustomCodeReference(new QCodeReference()), + "not a possibleValueProvider", + "missing a code reference name", + "missing a code type"); + } + + + /******************************************************************************* ** Run a little setup code on a qInstance; then validate it, and assert that it ** failed validation with reasons that match the supplied vararg-reasons (but allow @@ -448,7 +689,8 @@ class QInstanceValidatorTest { if(!allowExtraReasons) { - assertEquals(reasons.length, e.getReasons().size(), "Expected number of validation failure reasons\nExpected: " + String.join(",", reasons) + "\nActual: " + e.getReasons()); + int noOfReasons = e.getReasons() == null ? 0 : e.getReasons().size(); + assertEquals(reasons.length, noOfReasons, "Expected number of validation failure reasons.\nExpected reasons: " + String.join(",", reasons) + "\nActual reasons: " + e.getReasons()); } for(String reason : reasons) @@ -460,6 +702,25 @@ class QInstanceValidatorTest + /******************************************************************************* + ** Assert that an instance is valid! + *******************************************************************************/ + private void assertValidationSuccess(Consumer setup) + { + try + { + QInstance qInstance = TestUtils.defineInstance(); + setup.accept(qInstance); + new QInstanceValidator().validate(qInstance); + } + catch(QInstanceValidationException e) + { + fail("Expected no validation errors, but received: " + e.getMessage()); + } + } + + + /******************************************************************************* ** utility method for asserting that a specific reason string is found within ** the list of reasons in the QInstanceValidationException. diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryBackendModuleTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryBackendModuleTest.java new file mode 100644 index 00000000..9bc5081f --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryBackendModuleTest.java @@ -0,0 +1,334 @@ +/* + * 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.modules.backend.implementations.memory; + + +import java.util.List; +import java.util.function.Function; +import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers; +import com.kingsrook.qqq.backend.core.actions.tables.CountAction; +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.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteOutput; +import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput; +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.tables.update.UpdateInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateOutput; +import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeUsage; +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.utils.TestUtils; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + + +/******************************************************************************* + ** Unit test for MemoryBackendModule + *******************************************************************************/ +class MemoryBackendModuleTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @BeforeEach + @AfterEach + void beforeAndAfter() + { + MemoryRecordStore.getInstance().reset(); + MemoryRecordStore.resetStatistics(); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testFullCRUD() throws QException + { + QInstance qInstance = TestUtils.defineInstance(); + QTableMetaData table = qInstance.getTable(TestUtils.TABLE_NAME_SHAPE); + QSession session = new QSession(); + + ///////////////////////// + // do an initial count // + ///////////////////////// + CountInput countInput = new CountInput(qInstance); + countInput.setSession(session); + countInput.setTableName(table.getName()); + assertEquals(0, new CountAction().execute(countInput).getCount()); + + ////////////////// + // do an insert // + ////////////////// + InsertInput insertInput = new InsertInput(qInstance); + insertInput.setSession(session); + insertInput.setTableName(table.getName()); + insertInput.setRecords(getTestRecords(table)); + InsertOutput insertOutput = new InsertAction().execute(insertInput); + assertEquals(3, insertOutput.getRecords().size()); + assertTrue(insertOutput.getRecords().stream().allMatch(r -> r.getValue("id") != null)); + assertTrue(insertOutput.getRecords().stream().anyMatch(r -> r.getValueInteger("id").equals(1))); + assertTrue(insertOutput.getRecords().stream().anyMatch(r -> r.getValueInteger("id").equals(2))); + assertTrue(insertOutput.getRecords().stream().anyMatch(r -> r.getValueInteger("id").equals(3))); + + //////////////// + // do a query // + //////////////// + QueryInput queryInput = new QueryInput(qInstance); + queryInput.setSession(session); + queryInput.setTableName(table.getName()); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + assertEquals(3, queryOutput.getRecords().size()); + assertTrue(queryOutput.getRecords().stream().allMatch(r -> r.getValue("id") != null)); + assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueInteger("id").equals(1))); + assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueInteger("id").equals(2))); + assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueInteger("id").equals(3))); + assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueString("name").equals("My Triangle"))); + assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueString("name").equals("Your Square"))); + assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueString("name").equals("Some Circle"))); + + assertEquals(3, new CountAction().execute(countInput).getCount()); + + ////////////////// + // do an update // + ////////////////// + UpdateInput updateInput = new UpdateInput(qInstance); + updateInput.setSession(session); + updateInput.setTableName(table.getName()); + updateInput.setRecords(List.of( + new QRecord() + .withTableName(table.getName()) + .withValue("id", 1) + .withValue("name", "Not My Triangle any more"), + new QRecord() + .withTableName(table.getName()) + .withValue("id", 3) + .withValue("type", "ellipse") + )); + UpdateOutput updateOutput = new UpdateAction().execute(updateInput); + assertEquals(2, updateOutput.getRecords().size()); + + queryOutput = new QueryAction().execute(queryInput); + assertEquals(3, queryOutput.getRecords().size()); + assertTrue(queryOutput.getRecords().stream().noneMatch(r -> r.getValueString("name").equals("My Triangle"))); + assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueString("name").equals("Not My Triangle any more"))); + assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueString("type").equals("ellipse"))); + assertTrue(queryOutput.getRecords().stream().noneMatch(r -> r.getValueString("type").equals("circle"))); + + assertEquals(3, new CountAction().execute(countInput).getCount()); + + ///////////////////////// + // do a filtered query // + ///////////////////////// + queryInput = new QueryInput(qInstance); + queryInput.setSession(session); + queryInput.setTableName(table.getName()); + queryInput.setFilter(new QQueryFilter().withCriteria(new QFilterCriteria("id", QCriteriaOperator.IN, List.of(1, 3)))); + queryOutput = new QueryAction().execute(queryInput); + assertEquals(2, queryOutput.getRecords().size()); + assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueInteger("id").equals(1))); + assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueInteger("id").equals(3))); + + ///////////////////////// + // do a filtered count // + ///////////////////////// + countInput.setFilter(queryInput.getFilter()); + assertEquals(2, new CountAction().execute(countInput).getCount()); + + ///////////////// + // do a delete // + ///////////////// + DeleteInput deleteInput = new DeleteInput(qInstance); + deleteInput.setSession(session); + deleteInput.setTableName(table.getName()); + deleteInput.setPrimaryKeys(List.of(1, 2)); + DeleteOutput deleteOutput = new DeleteAction().execute(deleteInput); + assertEquals(2, deleteOutput.getDeletedRecordCount()); + + assertEquals(1, new CountAction().execute(countInput).getCount()); + + queryOutput = new QueryAction().execute(queryInput); + assertEquals(1, queryOutput.getRecords().size()); + assertTrue(queryOutput.getRecords().stream().noneMatch(r -> r.getValueInteger("id").equals(1))); + assertTrue(queryOutput.getRecords().stream().noneMatch(r -> r.getValueInteger("id").equals(2))); + assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueInteger("id").equals(3))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testSerials() throws QException + { + QInstance qInstance = TestUtils.defineInstance(); + QTableMetaData table = qInstance.getTable(TestUtils.TABLE_NAME_SHAPE); + QSession session = new QSession(); + + ////////////////// + // do an insert // + ////////////////// + InsertInput insertInput = new InsertInput(qInstance); + insertInput.setSession(session); + insertInput.setTableName(table.getName()); + insertInput.setRecords(List.of(new QRecord().withTableName(table.getName()).withValue("name", "Shape 1"))); + new InsertAction().execute(insertInput); + + insertInput.setRecords(List.of(new QRecord().withTableName(table.getName()).withValue("name", "Shape 2"))); + new InsertAction().execute(insertInput); + + insertInput.setRecords(List.of(new QRecord().withTableName(table.getName()).withValue("name", "Shape 3"))); + new InsertAction().execute(insertInput); + + QueryInput queryInput = new QueryInput(qInstance); + queryInput.setSession(new QSession()); + queryInput.setTableName(table.getName()); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueInteger("id").equals(1))); + assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueInteger("id").equals(2))); + assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueInteger("id").equals(3))); + + insertInput.setRecords(List.of(new QRecord().withTableName(table.getName()).withValue("id", 4).withValue("name", "Shape 4"))); + new InsertAction().execute(insertInput); + queryOutput = new QueryAction().execute(queryInput); + assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueInteger("id").equals(4))); + + insertInput.setRecords(List.of(new QRecord().withTableName(table.getName()).withValue("id", 6).withValue("name", "Shape 6"))); + new InsertAction().execute(insertInput); + queryOutput = new QueryAction().execute(queryInput); + assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueInteger("id").equals(6))); + + insertInput.setRecords(List.of(new QRecord().withTableName(table.getName()).withValue("name", "Shape 7"))); + new InsertAction().execute(insertInput); + queryOutput = new QueryAction().execute(queryInput); + assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueInteger("id").equals(7))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private List getTestRecords(QTableMetaData table) + { + return List.of( + new QRecord() + .withTableName(table.getName()) + .withValue("name", "My Triangle") + .withValue("type", "triangle") + .withValue("noOfSides", 3) + .withValue("isPolygon", true), + new QRecord() + .withTableName(table.getName()) + .withValue("name", "Your Square") + .withValue("type", "square") + .withValue("noOfSides", 4) + .withValue("isPolygon", true), + new QRecord() + .withTableName(table.getName()) + .withValue("name", "Some Circle") + .withValue("type", "circle") + .withValue("noOfSides", null) + .withValue("isPolygon", false) + ); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testCustomizer() throws QException + { + QInstance qInstance = TestUtils.defineInstance(); + QTableMetaData table = qInstance.getTable(TestUtils.TABLE_NAME_SHAPE); + QSession session = new QSession(); + + /////////////////////////////////// + // add a customizer to the table // + /////////////////////////////////// + table.withCustomizer(TableCustomizers.POST_QUERY_RECORD.getRole(), new QCodeReference(ShapeTestCustomizer.class, QCodeUsage.CUSTOMIZER)); + + ////////////////// + // do an insert // + ////////////////// + InsertInput insertInput = new InsertInput(qInstance); + insertInput.setSession(session); + insertInput.setTableName(table.getName()); + insertInput.setRecords(getTestRecords(table)); + new InsertAction().execute(insertInput); + + /////////////////////////////////////////////////////// + // do a query - assert that the customizer did stuff // + /////////////////////////////////////////////////////// + ShapeTestCustomizer.executionCount = 0; + QueryInput queryInput = new QueryInput(qInstance); + queryInput.setSession(session); + queryInput.setTableName(table.getName()); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + assertEquals(3, queryOutput.getRecords().size()); + assertEquals(3, ShapeTestCustomizer.executionCount); + assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueInteger("id").equals(1) && r.getValueInteger("tenTimesId").equals(10))); + assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueInteger("id").equals(2) && r.getValueInteger("tenTimesId").equals(20))); + assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueInteger("id").equals(3) && r.getValueInteger("tenTimesId").equals(30))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static class ShapeTestCustomizer implements Function + { + static int executionCount = 0; + + + + @Override + public QRecord apply(QRecord record) + { + executionCount++; + record.setValue("tenTimesId", record.getValueInteger("id") * 10); + return (record); + } + } +} \ No newline at end of file diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java index a0711f87..9058bc4a 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 @@ -22,12 +22,15 @@ package com.kingsrook.qqq.backend.core.utils; +import java.io.Serializable; import java.util.List; import com.kingsrook.qqq.backend.core.actions.processes.person.addtopeoplesage.AddAge; import com.kingsrook.qqq.backend.core.actions.processes.person.addtopeoplesage.GetAgeStatistics; +import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; -import com.kingsrook.qqq.backend.core.adapters.QInstanceAdapter; +import com.kingsrook.qqq.backend.core.actions.values.QCustomPossibleValueProvider; import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; 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.data.QRecord; @@ -40,6 +43,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeUsage; 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.layout.QAppMetaData; +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.processes.QBackendStepMetaData; @@ -52,6 +56,7 @@ 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.MockAuthenticationModule; import com.kingsrook.qqq.backend.core.modules.authentication.metadata.QAuthenticationMetaData; +import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryBackendModule; import com.kingsrook.qqq.backend.core.modules.backend.implementations.mock.MockBackendModule; import com.kingsrook.qqq.backend.core.processes.implementations.etl.basic.BasicETLProcess; import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamed.StreamedETLProcess; @@ -65,12 +70,14 @@ import com.kingsrook.qqq.backend.core.processes.implementations.mock.MockBackend public class TestUtils { public static final String DEFAULT_BACKEND_NAME = "default"; + public static final String MEMORY_BACKEND_NAME = "memory"; public static final String APP_NAME_GREETINGS = "greetingsApp"; public static final String APP_NAME_PEOPLE = "peopleApp"; public static final String APP_NAME_MISCELLANEOUS = "miscellaneous"; public static final String TABLE_NAME_PERSON = "person"; + public static final String TABLE_NAME_SHAPE = "shape"; public static final String PROCESS_NAME_GREET_PEOPLE = "greet"; public static final String PROCESS_NAME_GREET_PEOPLE_INTERACTIVE = "greetInteractive"; @@ -78,6 +85,10 @@ public class TestUtils public static final String TABLE_NAME_PERSON_FILE = "personFile"; public static final String TABLE_NAME_ID_AND_NAME_ONLY = "idAndNameOnly"; + public static final String POSSIBLE_VALUE_SOURCE_STATE = "state"; // enum-type + public static final String POSSIBLE_VALUE_SOURCE_SHAPE = "shape"; // table-type + public static final String POSSIBLE_VALUE_SOURCE_CUSTOM = "custom"; // custom-type + /******************************************************************************* @@ -89,12 +100,16 @@ public class TestUtils QInstance qInstance = new QInstance(); qInstance.setAuthentication(defineAuthentication()); qInstance.addBackend(defineBackend()); + qInstance.addBackend(defineMemoryBackend()); qInstance.addTable(defineTablePerson()); qInstance.addTable(definePersonFileTable()); qInstance.addTable(defineTableIdAndNameOnly()); + qInstance.addTable(defineTableShape()); qInstance.addPossibleValueSource(defineStatesPossibleValueSource()); + qInstance.addPossibleValueSource(defineShapePossibleValueSource()); + qInstance.addPossibleValueSource(defineCustomPossibleValueSource()); qInstance.addProcess(defineProcessGreetPeople()); qInstance.addProcess(defineProcessGreetPeopleInteractive()); @@ -104,8 +119,6 @@ public class TestUtils defineApps(qInstance); - System.out.println(new QInstanceAdapter().qInstanceToJson(qInstance)); - return (qInstance); } @@ -139,12 +152,40 @@ public class TestUtils ** Define the "states" possible value source used in standard tests ** *******************************************************************************/ - private static QPossibleValueSource defineStatesPossibleValueSource() + private static QPossibleValueSource defineStatesPossibleValueSource() { - return new QPossibleValueSource() - .withName("state") + return new QPossibleValueSource() + .withName(POSSIBLE_VALUE_SOURCE_STATE) .withType(QPossibleValueSourceType.ENUM) - .withEnumValues(List.of("IL", "MO")); + .withEnumValues(List.of(new QPossibleValue<>(1, "IL"), new QPossibleValue<>(2, "MO"))); + } + + + + /******************************************************************************* + ** Define the "shape" possible value source used in standard tests + ** + *******************************************************************************/ + private static QPossibleValueSource defineShapePossibleValueSource() + { + return new QPossibleValueSource() + .withName(POSSIBLE_VALUE_SOURCE_SHAPE) + .withType(QPossibleValueSourceType.TABLE) + .withTableName(TABLE_NAME_SHAPE); + } + + + + /******************************************************************************* + ** Define the "custom" possible value source used in standard tests + ** + *******************************************************************************/ + private static QPossibleValueSource defineCustomPossibleValueSource() + { + return new QPossibleValueSource() + .withName(POSSIBLE_VALUE_SOURCE_CUSTOM) + .withType(QPossibleValueSourceType.CUSTOM) + .withCustomCodeReference(new QCodeReference(CustomPossibleValueSource.class)); } @@ -174,6 +215,18 @@ public class TestUtils + /******************************************************************************* + ** Define the in-memory backend used in standard tests + *******************************************************************************/ + public static QBackendMetaData defineMemoryBackend() + { + return new QBackendMetaData() + .withName(MEMORY_BACKEND_NAME) + .withBackendType(MemoryBackendModule.class); + } + + + /******************************************************************************* ** Define the 'person' table used in standard tests. *******************************************************************************/ @@ -191,7 +244,32 @@ public class TestUtils .withField(new QFieldMetaData("lastName", QFieldType.STRING)) .withField(new QFieldMetaData("birthDate", QFieldType.DATE)) .withField(new QFieldMetaData("email", QFieldType.STRING)) - .withField(new QFieldMetaData("homeState", QFieldType.STRING).withPossibleValueSourceName("state")); + .withField(new QFieldMetaData("homeStateId", QFieldType.INTEGER).withPossibleValueSourceName(POSSIBLE_VALUE_SOURCE_STATE)) + .withField(new QFieldMetaData("favoriteShapeId", QFieldType.INTEGER).withPossibleValueSourceName(POSSIBLE_VALUE_SOURCE_SHAPE)) + .withField(new QFieldMetaData("customValue", QFieldType.INTEGER).withPossibleValueSourceName(POSSIBLE_VALUE_SOURCE_CUSTOM)) + ; + } + + + + /******************************************************************************* + ** Define the 'shape' table used in standard tests. + *******************************************************************************/ + public static QTableMetaData defineTableShape() + { + return new QTableMetaData() + .withName(TABLE_NAME_SHAPE) + .withBackendName(MEMORY_BACKEND_NAME) + .withPrimaryKeyField("id") + .withRecordLabelFields("name") + .withField(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false)) + .withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME).withIsEditable(false)) + .withField(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME).withIsEditable(false)) + .withField(new QFieldMetaData("name", QFieldType.STRING)) + .withField(new QFieldMetaData("type", QFieldType.STRING)) // todo PVS + .withField(new QFieldMetaData("noOfSides", QFieldType.INTEGER)) + .withField(new QFieldMetaData("isPolygon", QFieldType.BOOLEAN)) // mmm, should be derived from type, no? + ; } @@ -371,6 +449,25 @@ public class TestUtils + /******************************************************************************* + ** + *******************************************************************************/ + public static void insertDefaultShapes(QInstance qInstance) throws QException + { + List shapeRecords = List.of( + new QRecord().withTableName(TABLE_NAME_SHAPE).withValue("id", 1).withValue("name", "Triangle"), + new QRecord().withTableName(TABLE_NAME_SHAPE).withValue("id", 2).withValue("name", "Square"), + new QRecord().withTableName(TABLE_NAME_SHAPE).withValue("id", 3).withValue("name", "Circle")); + + InsertInput insertInput = new InsertInput(qInstance); + insertInput.setSession(new QSession()); + insertInput.setTableName(TABLE_NAME_SHAPE); + insertInput.setRecords(shapeRecords); + new InsertAction().execute(insertInput); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -417,4 +514,21 @@ public class TestUtils """); } + + + /******************************************************************************* + ** + *******************************************************************************/ + public static class CustomPossibleValueSource implements QCustomPossibleValueProvider + { + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public QPossibleValue getPossibleValue(Serializable idValue) + { + return (new QPossibleValue<>(idValue, "Custom[" + idValue + "]")); + } + } } diff --git a/qqq-backend-core/src/test/resources/personQInstance.json b/qqq-backend-core/src/test/resources/personQInstance.json index 9b1d333b..a0d2f454 100644 --- a/qqq-backend-core/src/test/resources/personQInstance.json +++ b/qqq-backend-core/src/test/resources/personQInstance.json @@ -60,8 +60,8 @@ "type": "STRING", "possibleValueSourceName": null }, - "homeState": { - "name": "homeState", + "homeStateId": { + "name": "homeStateId", "label": null, "backendName": null, "type": "STRING", diff --git a/qqq-backend-core/src/test/resources/personQInstanceIncludingBackend.json b/qqq-backend-core/src/test/resources/personQInstanceIncludingBackend.json index 5502e715..bda0d747 100644 --- a/qqq-backend-core/src/test/resources/personQInstanceIncludingBackend.json +++ b/qqq-backend-core/src/test/resources/personQInstanceIncludingBackend.json @@ -27,8 +27,8 @@ "type": "DATE_TIME", "possibleValueSourceName": null }, - "homeState": { - "name": "homeState", + "homeStateId": { + "name": "homeStateId", "backendName": null, "label": null, "type": "STRING", diff --git a/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/base/FilesystemBackendModuleInterface.java b/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/base/FilesystemBackendModuleInterface.java index 23c38c46..816caa06 100644 --- a/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/base/FilesystemBackendModuleInterface.java +++ b/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/base/FilesystemBackendModuleInterface.java @@ -31,11 +31,11 @@ import com.kingsrook.qqq.backend.module.filesystem.base.actions.AbstractBaseFile *******************************************************************************/ public interface FilesystemBackendModuleInterface { - String CUSTOMIZER_FILE_POST_FILE_READ = "postFileRead"; /******************************************************************************* ** For filesystem backends, get the module-specific action base-class, that helps ** with functions like listing and deleting files. *******************************************************************************/ AbstractBaseFilesystemAction getActionBase(); + } diff --git a/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/base/actions/AbstractBaseFilesystemAction.java b/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/base/actions/AbstractBaseFilesystemAction.java index 02b26622..3da02b62 100644 --- a/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/base/actions/AbstractBaseFilesystemAction.java +++ b/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/base/actions/AbstractBaseFilesystemAction.java @@ -31,6 +31,8 @@ import java.util.function.Function; import com.kingsrook.qqq.backend.core.adapters.CsvToQRecordAdapter; import com.kingsrook.qqq.backend.core.adapters.JsonToQRecordAdapter; import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput; 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.data.QRecord; @@ -40,7 +42,6 @@ import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableBackendDetails; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.utils.StringUtils; -import com.kingsrook.qqq.backend.module.filesystem.base.FilesystemBackendModuleInterface; import com.kingsrook.qqq.backend.module.filesystem.base.FilesystemRecordBackendDetailFields; import com.kingsrook.qqq.backend.module.filesystem.base.model.metadata.AbstractFilesystemBackendMetaData; import com.kingsrook.qqq.backend.module.filesystem.base.model.metadata.AbstractFilesystemTableBackendDetails; @@ -203,7 +204,13 @@ public abstract class AbstractBaseFilesystemAction if(queryInput.getRecordPipe() != null) { - new CsvToQRecordAdapter().buildRecordsFromCsv(queryInput.getRecordPipe(), fileContents, table, null, (record -> addBackendDetailsToRecord(record, file))); + new CsvToQRecordAdapter().buildRecordsFromCsv(queryInput.getRecordPipe(), fileContents, table, null, (record -> + { + //////////////////////////////////////////////////////////////////////////////////////////// + // Before the records go into the pipe, make sure their backend details are added to them // + //////////////////////////////////////////////////////////////////////////////////////////// + addBackendDetailsToRecord(record, file); + })); } else { @@ -243,6 +250,24 @@ public abstract class AbstractBaseFilesystemAction + /******************************************************************************* + ** + *******************************************************************************/ + public CountOutput executeCount(CountInput countInput) throws QException + { + QueryInput queryInput = new QueryInput(countInput.getInstance()); + queryInput.setSession(countInput.getSession()); + queryInput.setTableName(countInput.getTableName()); + queryInput.setFilter(countInput.getFilter()); + QueryOutput queryOutput = executeQuery(queryInput); + + CountOutput countOutput = new CountOutput(); + countOutput.setCount(queryOutput.getRecords().size()); + return (countOutput); + } + + + /******************************************************************************* ** Add backend details to records about the file that they are in. *******************************************************************************/ @@ -281,7 +306,7 @@ public abstract class AbstractBaseFilesystemAction *******************************************************************************/ private String customizeFileContentsAfterReading(QTableMetaData table, String fileContents) throws QException { - Optional optionalCustomizer = table.getCustomizer(FilesystemBackendModuleInterface.CUSTOMIZER_FILE_POST_FILE_READ); + Optional optionalCustomizer = table.getCustomizer(FilesystemTableCustomizers.POST_READ_FILE.getRole()); if(optionalCustomizer.isEmpty()) { return (fileContents); diff --git a/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/base/actions/FilesystemTableCustomizers.java b/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/base/actions/FilesystemTableCustomizers.java new file mode 100644 index 00000000..06157ceb --- /dev/null +++ b/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/base/actions/FilesystemTableCustomizers.java @@ -0,0 +1,72 @@ +package com.kingsrook.qqq.backend.module.filesystem.base.actions; + + +import java.util.function.Function; +import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizer; + + +/******************************************************************************* + ** + *******************************************************************************/ +public enum FilesystemTableCustomizers +{ + POST_READ_FILE(new TableCustomizer("postReadFile", Function.class, ((Object x) -> + { + Function function = (Function) x; + String output = function.apply(new String()); + }))); + + private final TableCustomizer tableCustomizer; + + + + /******************************************************************************* + ** + *******************************************************************************/ + FilesystemTableCustomizers(TableCustomizer tableCustomizer) + { + this.tableCustomizer = tableCustomizer; + } + + + + /******************************************************************************* + ** Get the FilesystemTableCustomer for a given role (e.g., the role used in meta-data, not + ** the enum-constant name). + *******************************************************************************/ + public static FilesystemTableCustomizers forRole(String name) + { + for(FilesystemTableCustomizers value : values()) + { + if(value.tableCustomizer.getRole().equals(name)) + { + return (value); + } + } + + return (null); + } + + + + /******************************************************************************* + ** Getter for tableCustomizer + ** + *******************************************************************************/ + public TableCustomizer getTableCustomizer() + { + return tableCustomizer; + } + + + + /******************************************************************************* + ** get the role from the tableCustomizer + ** + *******************************************************************************/ + public String getRole() + { + return (tableCustomizer.getRole()); + } + +} diff --git a/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/local/FilesystemBackendModule.java b/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/local/FilesystemBackendModule.java index 2bc14244..493a08ac 100644 --- a/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/local/FilesystemBackendModule.java +++ b/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/local/FilesystemBackendModule.java @@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.module.filesystem.local; import java.io.File; +import com.kingsrook.qqq.backend.core.actions.interfaces.CountInterface; import com.kingsrook.qqq.backend.core.actions.interfaces.DeleteInterface; import com.kingsrook.qqq.backend.core.actions.interfaces.InsertInterface; import com.kingsrook.qqq.backend.core.actions.interfaces.QueryInterface; @@ -33,6 +34,7 @@ import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface; import com.kingsrook.qqq.backend.module.filesystem.base.FilesystemBackendModuleInterface; import com.kingsrook.qqq.backend.module.filesystem.base.actions.AbstractBaseFilesystemAction; import com.kingsrook.qqq.backend.module.filesystem.local.actions.AbstractFilesystemAction; +import com.kingsrook.qqq.backend.module.filesystem.local.actions.FilesystemCountAction; import com.kingsrook.qqq.backend.module.filesystem.local.actions.FilesystemDeleteAction; import com.kingsrook.qqq.backend.module.filesystem.local.actions.FilesystemInsertAction; import com.kingsrook.qqq.backend.module.filesystem.local.actions.FilesystemQueryAction; @@ -107,6 +109,16 @@ public class FilesystemBackendModule implements QBackendModuleInterface, Filesys } + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public CountInterface getCountInterface() + { + return new FilesystemCountAction(); + } + + /******************************************************************************* ** diff --git a/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/FilesystemCountAction.java b/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/FilesystemCountAction.java new file mode 100644 index 00000000..d93c7da8 --- /dev/null +++ b/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/FilesystemCountAction.java @@ -0,0 +1,45 @@ +/* + * 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.module.filesystem.local.actions; + + +import com.kingsrook.qqq.backend.core.actions.interfaces.CountInterface; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class FilesystemCountAction extends AbstractFilesystemAction implements CountInterface +{ + + /******************************************************************************* + ** + *******************************************************************************/ + public CountOutput execute(CountInput countInput) throws QException + { + return (executeCount(countInput)); + } + +} diff --git a/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/s3/actions/S3CountAction.java b/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/s3/actions/S3CountAction.java new file mode 100644 index 00000000..71854aa9 --- /dev/null +++ b/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/s3/actions/S3CountAction.java @@ -0,0 +1,45 @@ +/* + * 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.module.filesystem.s3.actions; + + +import com.kingsrook.qqq.backend.core.actions.interfaces.CountInterface; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class S3CountAction extends AbstractS3Action implements CountInterface +{ + + /******************************************************************************* + ** + *******************************************************************************/ + public CountOutput execute(CountInput countInput) throws QException + { + return (executeCount(countInput)); + } + +} diff --git a/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/FilesystemActionTest.java b/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/FilesystemActionTest.java index 94cbfbf7..8cf24310 100644 --- a/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/FilesystemActionTest.java +++ b/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/FilesystemActionTest.java @@ -73,11 +73,14 @@ public class FilesystemActionTest TestUtils.increaseTestInstanceCounter(); FilesystemBackendMetaData filesystemBackendMetaData = TestUtils.defineLocalFilesystemBackend(); - File baseDirectory = new File(filesystemBackendMetaData.getBasePath()); - boolean mkdirsResult = baseDirectory.mkdirs(); - if(!mkdirsResult) + File baseDirectory = new File(filesystemBackendMetaData.getBasePath()); + if(!baseDirectory.exists()) { - fail("Failed to make directories at [" + baseDirectory + "] for filesystem backend module"); + boolean mkdirsResult = baseDirectory.mkdirs(); + if(!mkdirsResult) + { + fail("Failed to make directories at [" + baseDirectory + "] for filesystem backend module"); + } } writePersonJSONFiles(baseDirectory); @@ -92,9 +95,9 @@ public class FilesystemActionTest private void writePersonJSONFiles(File baseDirectory) throws IOException { String fullPath = baseDirectory.getAbsolutePath(); - if (TestUtils.defineLocalFilesystemJSONPersonTable().getBackendDetails() instanceof FilesystemTableBackendDetails details) + if(TestUtils.defineLocalFilesystemJSONPersonTable().getBackendDetails() instanceof FilesystemTableBackendDetails details) { - if (StringUtils.hasContent(details.getBasePath())) + if(StringUtils.hasContent(details.getBasePath())) { fullPath += File.separatorChar + details.getBasePath(); } @@ -125,9 +128,9 @@ public class FilesystemActionTest private void writePersonCSVFiles(File baseDirectory) throws IOException { String fullPath = baseDirectory.getAbsolutePath(); - if (TestUtils.defineLocalFilesystemCSVPersonTable().getBackendDetails() instanceof FilesystemTableBackendDetails details) + if(TestUtils.defineLocalFilesystemCSVPersonTable().getBackendDetails() instanceof FilesystemTableBackendDetails details) { - if (StringUtils.hasContent(details.getBasePath())) + if(StringUtils.hasContent(details.getBasePath())) { fullPath += File.separatorChar + details.getBasePath(); } diff --git a/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/FilesystemCountActionTest.java b/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/FilesystemCountActionTest.java new file mode 100644 index 00000000..46cfabe2 --- /dev/null +++ b/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/FilesystemCountActionTest.java @@ -0,0 +1,52 @@ +/* + * 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.module.filesystem.local.actions; + + +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput; +import com.kingsrook.qqq.backend.module.filesystem.TestUtils; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + + +/******************************************************************************* + ** Unit test for FilesystemCountAction + *******************************************************************************/ +public class FilesystemCountActionTest extends FilesystemActionTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + public void testCount1() throws QException + { + CountInput countInput = new CountInput(); + countInput.setInstance(TestUtils.defineInstance()); + countInput.setTableName(TestUtils.defineLocalFilesystemJSONPersonTable().getName()); + CountOutput countOutput = new FilesystemCountAction().execute(countInput); + Assertions.assertEquals(3, countOutput.getCount(), "Unfiltered count should find all rows"); + } + +} \ No newline at end of file diff --git a/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/FilesystemDeleteActionTest.java b/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/FilesystemDeleteActionTest.java new file mode 100644 index 00000000..e43a667e --- /dev/null +++ b/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/FilesystemDeleteActionTest.java @@ -0,0 +1,47 @@ +/* + * 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.module.filesystem.local.actions; + + +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput; +import org.apache.commons.lang.NotImplementedException; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertThrows; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class FilesystemDeleteActionTest extends FilesystemActionTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + public void test() throws QException + { + assertThrows(NotImplementedException.class, () -> new FilesystemDeleteAction().execute(new DeleteInput())); + } + +} \ No newline at end of file diff --git a/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/FilesystemInsertActionTest.java b/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/FilesystemInsertActionTest.java new file mode 100644 index 00000000..92d38604 --- /dev/null +++ b/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/FilesystemInsertActionTest.java @@ -0,0 +1,47 @@ +/* + * 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.module.filesystem.local.actions; + + +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; +import org.apache.commons.lang.NotImplementedException; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertThrows; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class FilesystemInsertActionTest extends FilesystemActionTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + public void test() throws QException + { + assertThrows(NotImplementedException.class, () -> new FilesystemInsertAction().execute(new InsertInput())); + } + +} \ No newline at end of file diff --git a/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/FilesystemQueryActionTest.java b/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/FilesystemQueryActionTest.java index 90771e43..f6646085 100644 --- a/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/FilesystemQueryActionTest.java +++ b/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/FilesystemQueryActionTest.java @@ -26,14 +26,13 @@ import java.util.function.Function; 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.tables.query.QueryOutput; -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.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeUsage; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.module.filesystem.TestUtils; -import com.kingsrook.qqq.backend.module.filesystem.base.FilesystemBackendModuleInterface; import com.kingsrook.qqq.backend.module.filesystem.base.FilesystemRecordBackendDetailFields; +import com.kingsrook.qqq.backend.module.filesystem.base.actions.FilesystemTableCustomizers; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -72,10 +71,7 @@ public class FilesystemQueryActionTest extends FilesystemActionTest QInstance instance = TestUtils.defineInstance(); QTableMetaData table = instance.getTable(TestUtils.TABLE_NAME_PERSON_LOCAL_FS_JSON); - table.withCustomizer(FilesystemBackendModuleInterface.CUSTOMIZER_FILE_POST_FILE_READ, new QCodeReference() - .withName(ValueUpshifter.class.getName()) - .withCodeType(QCodeType.JAVA) - .withCodeUsage(QCodeUsage.CUSTOMIZER)); + table.withCustomizer(FilesystemTableCustomizers.POST_READ_FILE.getRole(), new QCodeReference(ValueUpshifter.class, QCodeUsage.CUSTOMIZER)); queryInput.setInstance(instance); queryInput.setTableName(TestUtils.defineLocalFilesystemJSONPersonTable().getName()); diff --git a/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/FilesystemUpdateActionTest.java b/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/FilesystemUpdateActionTest.java new file mode 100644 index 00000000..25d39070 --- /dev/null +++ b/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/FilesystemUpdateActionTest.java @@ -0,0 +1,47 @@ +/* + * 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.module.filesystem.local.actions; + + +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput; +import org.apache.commons.lang.NotImplementedException; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertThrows; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class FilesystemUpdateActionTest extends FilesystemActionTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + public void test() throws QException + { + assertThrows(NotImplementedException.class, () -> new FilesystemUpdateAction().execute(new UpdateInput())); + } + +} \ No newline at end of file diff --git a/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/s3/BaseS3Test.java b/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/s3/BaseS3Test.java index 4a9c979b..413f7ae8 100644 --- a/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/s3/BaseS3Test.java +++ b/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/s3/BaseS3Test.java @@ -31,8 +31,6 @@ import com.amazonaws.services.s3.model.S3ObjectSummary; import com.kingsrook.qqq.backend.module.filesystem.s3.utils.S3Utils; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.condition.DisabledOnOs; -import org.junit.jupiter.api.condition.OS; import org.junit.jupiter.api.extension.ExtendWith; @@ -40,7 +38,7 @@ import org.junit.jupiter.api.extension.ExtendWith; ** Base class for tests that want to be able to work with localstack s3. *******************************************************************************/ @ExtendWith(LocalstackDockerExtension.class) -@LocalstackDockerProperties(services = { ServiceName.S3 }, portEdge = "2960", portElasticSearch = "2961") +@LocalstackDockerProperties(useSingleDockerContainer = true, services = { ServiceName.S3 }, portEdge = "2960", portElasticSearch = "2961") public class BaseS3Test { public static final String BUCKET_NAME = "localstack-test-bucket"; diff --git a/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/s3/actions/S3CountActionTest.java b/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/s3/actions/S3CountActionTest.java new file mode 100644 index 00000000..b2538f43 --- /dev/null +++ b/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/s3/actions/S3CountActionTest.java @@ -0,0 +1,66 @@ +/* + * 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.module.filesystem.s3.actions; + + +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput; +import com.kingsrook.qqq.backend.module.filesystem.TestUtils; +import com.kingsrook.qqq.backend.module.filesystem.s3.BaseS3Test; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class S3CountActionTest extends BaseS3Test +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + public void testCount1() throws QException + { + CountInput countInput = initCountRequest(); + S3CountAction s3CountAction = new S3CountAction(); + s3CountAction.setS3Utils(getS3Utils()); + CountOutput countOutput = s3CountAction.execute(countInput); + Assertions.assertEquals(5, countOutput.getCount(), "Expected # of rows from unfiltered count"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private CountInput initCountRequest() throws QException + { + CountInput countInput = new CountInput(); + countInput.setInstance(TestUtils.defineInstance()); + countInput.setTableName(TestUtils.defineS3CSVPersonTable().getName()); + return countInput; + } + +} \ No newline at end of file diff --git a/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/s3/actions/S3DeleteActionTest.java b/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/s3/actions/S3DeleteActionTest.java new file mode 100644 index 00000000..6b1ba2fa --- /dev/null +++ b/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/s3/actions/S3DeleteActionTest.java @@ -0,0 +1,48 @@ +/* + * 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.module.filesystem.s3.actions; + + +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput; +import com.kingsrook.qqq.backend.module.filesystem.s3.BaseS3Test; +import org.apache.commons.lang.NotImplementedException; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertThrows; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class S3DeleteActionTest extends BaseS3Test +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + public void test() throws QException + { + assertThrows(NotImplementedException.class, () -> new S3DeleteAction().execute(new DeleteInput())); + } + +} \ No newline at end of file diff --git a/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/s3/actions/S3InsertActionTest.java b/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/s3/actions/S3InsertActionTest.java new file mode 100644 index 00000000..94e774f4 --- /dev/null +++ b/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/s3/actions/S3InsertActionTest.java @@ -0,0 +1,48 @@ +/* + * 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.module.filesystem.s3.actions; + + +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; +import com.kingsrook.qqq.backend.module.filesystem.s3.BaseS3Test; +import org.apache.commons.lang.NotImplementedException; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertThrows; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class S3InsertActionTest extends BaseS3Test +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + public void test() throws QException + { + assertThrows(NotImplementedException.class, () -> new S3InsertAction().execute(new InsertInput())); + } + +} \ No newline at end of file diff --git a/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/s3/actions/S3UpdateActionTest.java b/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/s3/actions/S3UpdateActionTest.java new file mode 100644 index 00000000..c2e0bcae --- /dev/null +++ b/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/s3/actions/S3UpdateActionTest.java @@ -0,0 +1,48 @@ +/* + * 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.module.filesystem.s3.actions; + + +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput; +import com.kingsrook.qqq.backend.module.filesystem.s3.BaseS3Test; +import org.apache.commons.lang.NotImplementedException; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertThrows; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class S3UpdateActionTest extends BaseS3Test +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + public void test() throws QException + { + assertThrows(NotImplementedException.class, () -> new S3UpdateAction().execute(new UpdateInput())); + } + +} \ No newline at end of file diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryAction.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryAction.java index 02718222..566a220e 100644 --- a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryAction.java +++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryAction.java @@ -200,6 +200,10 @@ public class RDBMSQueryAction extends AbstractRDBMSAction implements QueryInterf { return (QueryManager.getLocalDateTime(resultSet, i)); } + case BOOLEAN: + { + return (QueryManager.getBoolean(resultSet, i)); + } default: { throw new IllegalStateException("Unexpected field type: " + qFieldMetaData.getType()); diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/QueryManager.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/QueryManager.java index b5572324..0a90182f 100644 --- a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/QueryManager.java +++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/QueryManager.java @@ -29,6 +29,7 @@ import java.sql.Connection; import java.sql.Date; import java.sql.PreparedStatement; import java.sql.ResultSet; +import java.sql.ResultSetMetaData; import java.sql.SQLException; import java.sql.Statement; import java.sql.Timestamp; @@ -277,8 +278,6 @@ public class QueryManager *******************************************************************************/ public static SimpleEntity executeStatementForSimpleEntity(Connection connection, String sql, Object... params) throws SQLException { - throw (new NotImplementedException()); - /* PreparedStatement statement = prepareStatementAndBindParams(connection, sql, params); statement.execute(); ResultSet resultSet = statement.getResultSet(); @@ -290,7 +289,6 @@ public class QueryManager { return (null); } - */ } @@ -355,8 +353,6 @@ public class QueryManager *******************************************************************************/ public static SimpleEntity buildSimpleEntity(ResultSet resultSet) throws SQLException { - throw (new NotImplementedException()); - /* SimpleEntity row = new SimpleEntity(); ResultSetMetaData metaData = resultSet.getMetaData(); @@ -365,7 +361,6 @@ public class QueryManager row.put(metaData.getColumnName(i), getObject(resultSet, i)); } return row; - */ } diff --git a/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/TestUtils.java b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/TestUtils.java index 18b38403..ce9b3ae5 100644 --- a/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/TestUtils.java +++ b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/TestUtils.java @@ -37,7 +37,7 @@ import com.kingsrook.qqq.backend.module.rdbms.jdbc.QueryManager; import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaData; import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSTableBackendDetails; import org.apache.commons.io.IOUtils; -import static junit.framework.Assert.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNotNull; /******************************************************************************* diff --git a/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryActionTest.java b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryActionTest.java index 3e4c37ba..d440dcc7 100644 --- a/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryActionTest.java +++ b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryActionTest.java @@ -428,12 +428,14 @@ public class RDBMSQueryActionTest extends RDBMSActionTest /******************************************************************************* ** This doesn't really test any RDBMS code, but is a checkpoint that the core - ** module is populating displayValues when it performs the system-level query action. + ** module is populating displayValues when it performs the system-level query action + ** (if so requested by input field). *******************************************************************************/ @Test public void testThatDisplayValuesGetSetGoingThroughQueryAction() throws QException { QueryInput queryInput = initQueryRequest(); + queryInput.setShouldGenerateDisplayValues(true); QueryOutput queryOutput = new QueryAction().execute(queryInput); Assertions.assertEquals(5, queryOutput.getRecords().size(), "Unfiltered query should find all rows"); diff --git a/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/QueryManagerTest.java b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/QueryManagerTest.java index ea5e99cc..b7be5476 100644 --- a/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/QueryManagerTest.java +++ b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/QueryManagerTest.java @@ -360,4 +360,25 @@ class QueryManagerTest assertEquals(null, QueryManager.executeStatementForSingleValue(connection, Integer.class, "SELECT int_col FROM test_table WHERE int_col IS NULL")); } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testQueryForSimpleEntity() throws SQLException + { + Connection connection = getConnection(); + QueryManager.executeUpdate(connection, """ + INSERT INTO test_table + ( int_col, datetime_col, char_col, date_col, time_col ) + VALUES + ( 47, '2022-08-10 19:22:08', 'Q', '2022-08-10', '19:22:08') + """); + SimpleEntity simpleEntity = QueryManager.executeStatementForSimpleEntity(connection, "SELECT * FROM test_table"); + assertNotNull(simpleEntity); + assertEquals(47, simpleEntity.get("INT_COL")); + assertEquals("Q", simpleEntity.get("CHAR_COL")); + } + } \ No newline at end of file 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 3e1fcf77..85b72727 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 @@ -34,6 +34,7 @@ import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.function.Supplier; import com.kingsrook.qqq.backend.core.actions.async.AsyncJobManager; import com.kingsrook.qqq.backend.core.actions.metadata.MetaDataAction; import com.kingsrook.qqq.backend.core.actions.metadata.ProcessMetaDataAction; @@ -109,11 +110,16 @@ public class QJavalinImplementation { private static final Logger LOG = LogManager.getLogger(QJavalinImplementation.class); - private static final int SESSION_COOKIE_AGE = 60 * 60 * 24; + private static final int SESSION_COOKIE_AGE = 60 * 60 * 24; private static final String SESSION_ID_COOKIE_NAME = "sessionId"; static QInstance qInstance; + private static Supplier qInstanceHotSwapSupplier; + private static long lastQInstanceHotSwapMillis; + + private static final long MILLIS_BETWEEN_HOT_SWAPS = 2500; + private static int DEFAULT_PORT = 8001; private static Javalin service; @@ -166,6 +172,50 @@ public class QJavalinImplementation // todo base path from arg? - and then potentially multiple instances too (chosen based on the root path??) service = Javalin.create().start(port); service.routes(getRoutes()); + service.before(QJavalinImplementation::hotSwapQInstance); + } + + + + /******************************************************************************* + ** If there's a qInstanceHotSwapSupplier, and its been a little while, replace + ** the qInstance with a new one from the supplier. Meant to be used while doing + ** development. + *******************************************************************************/ + public static void hotSwapQInstance(Context context) + { + if(qInstanceHotSwapSupplier != null) + { + long now = System.currentTimeMillis(); + if(now - lastQInstanceHotSwapMillis < MILLIS_BETWEEN_HOT_SWAPS) + { + return; + } + + lastQInstanceHotSwapMillis = now; + + try + { + QInstance newQInstance = qInstanceHotSwapSupplier.get(); + if(newQInstance == null) + { + LOG.warn("Got a null qInstance from hotSwapSupplier. Not hot-swapping."); + return; + } + + new QInstanceValidator().validate(newQInstance); + QJavalinImplementation.qInstance = newQInstance; + LOG.info("Swapped qInstance"); + } + catch(QInstanceValidationException e) + { + LOG.warn(e.getMessage()); + } + catch(Exception e) + { + LOG.error("Error swapping QInstance", e); + } + } } @@ -249,7 +299,7 @@ public class QJavalinImplementation static void setupSession(Context context, AbstractActionInput input) throws QModuleDispatchException { QAuthenticationModuleDispatcher qAuthenticationModuleDispatcher = new QAuthenticationModuleDispatcher(); - QAuthenticationModuleInterface authenticationModule = qAuthenticationModuleDispatcher.getQModule(input.getAuthenticationMetaData()); + QAuthenticationModuleInterface authenticationModule = qAuthenticationModuleDispatcher.getQModule(input.getAuthenticationMetaData()); try { @@ -266,7 +316,7 @@ public class QJavalinImplementation else { String authorizationHeaderValue = context.header("Authorization"); - if (authorizationHeaderValue != null) + if(authorizationHeaderValue != null) { String bearerPrefix = "Bearer "; if(authorizationHeaderValue.startsWith(bearerPrefix)) @@ -309,7 +359,7 @@ public class QJavalinImplementation { try { - String table = context.pathParam("table"); + String table = context.pathParam("table"); List primaryKeys = new ArrayList<>(); primaryKeys.add(context.pathParam("primaryKey")); @@ -338,9 +388,9 @@ public class QJavalinImplementation { try { - String table = context.pathParam("table"); + String table = context.pathParam("table"); List recordList = new ArrayList<>(); - QRecord record = new QRecord(); + QRecord record = new QRecord(); record.setTableName(table); recordList.add(record); @@ -382,9 +432,9 @@ public class QJavalinImplementation { try { - String table = context.pathParam("table"); + String table = context.pathParam("table"); List recordList = new ArrayList<>(); - QRecord record = new QRecord(); + QRecord record = new QRecord(); record.setTableName(table); recordList.add(record); @@ -429,6 +479,8 @@ public class QJavalinImplementation setupSession(context, queryInput); queryInput.setTableName(tableName); + queryInput.setShouldGenerateDisplayValues(true); + queryInput.setShouldTranslatePossibleValues(true); // todo - validate that the primary key is of the proper type (e.g,. not a string for an id field) // and throw a 400-series error (tell the user bad-request), rather than, we're doing a 500 (server error) @@ -524,6 +576,8 @@ public class QJavalinImplementation QueryInput queryInput = new QueryInput(qInstance); setupSession(context, queryInput); queryInput.setTableName(context.pathParam("table")); + queryInput.setShouldGenerateDisplayValues(true); + queryInput.setShouldTranslatePossibleValues(true); queryInput.setSkip(integerQueryParam(context, "skip")); queryInput.setLimit(integerQueryParam(context, "limit")); @@ -836,4 +890,14 @@ public class QJavalinImplementation return (null); } + + + /******************************************************************************* + ** Setter for qInstanceHotSwapSupplier + *******************************************************************************/ + public static void setQInstanceHotSwapSupplier(Supplier qInstanceHotSwapSupplier) + { + QJavalinImplementation.qInstanceHotSwapSupplier = qInstanceHotSwapSupplier; + } + } 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 818c2b53..10b8cc02 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 @@ -50,7 +50,7 @@ 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; import org.apache.commons.io.IOUtils; -import static junit.framework.Assert.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNotNull; /******************************************************************************* diff --git a/qqq-middleware-picocli/src/main/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementation.java b/qqq-middleware-picocli/src/main/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementation.java index e83fe394..a59cada8 100644 --- a/qqq-middleware-picocli/src/main/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementation.java +++ b/qqq-middleware-picocli/src/main/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementation.java @@ -536,6 +536,11 @@ public class QPicoCliImplementation queryInput.setSession(session); queryInput.setTableName(tableName); queryInput.setSkip(subParseResult.matchedOptionValue("skip", null)); + + // todo - think about these (e.g., based on user's requested output format? + // queryInput.setShouldGenerateDisplayValues(true); + // queryInput.setShouldTranslatePossibleValues(true); + String primaryKeyValue = subParseResult.matchedPositionalValue(0, null); if(primaryKeyValue == null) @@ -581,6 +586,10 @@ public class QPicoCliImplementation queryInput.setLimit(subParseResult.matchedOptionValue("limit", null)); queryInput.setFilter(generateQueryFilter(subParseResult)); + // todo - think about these (e.g., based on user's requested output format? + // queryInput.setShouldGenerateDisplayValues(true); + // queryInput.setShouldTranslatePossibleValues(true); + QueryAction queryAction = new QueryAction(); QueryOutput queryOutput = queryAction.execute(queryInput); commandLine.getOut().println(JsonUtils.toPrettyJson(queryOutput)); diff --git a/qqq-middleware-picocli/src/test/java/com/kingsrook/qqq/frontend/picocli/TestUtils.java b/qqq-middleware-picocli/src/test/java/com/kingsrook/qqq/frontend/picocli/TestUtils.java index 434b9d39..fb2aced0 100644 --- a/qqq-middleware-picocli/src/test/java/com/kingsrook/qqq/frontend/picocli/TestUtils.java +++ b/qqq-middleware-picocli/src/test/java/com/kingsrook/qqq/frontend/picocli/TestUtils.java @@ -44,7 +44,7 @@ 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; import org.apache.commons.io.IOUtils; -import static junit.framework.Assert.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNotNull; /******************************************************************************* diff --git a/qqq-sample-project/pom.xml b/qqq-sample-project/pom.xml index 917a1c4f..23f84047 100644 --- a/qqq-sample-project/pom.xml +++ b/qqq-sample-project/pom.xml @@ -98,6 +98,11 @@ assertj-core test + + org.liquibase + liquibase-core + 4.10.0 + @@ -116,6 +121,19 @@ + + + org.liquibase + liquibase-maven-plugin + 4.10.0 + + /src/main/resources/liquibase/liquibase.properties + ${env.LB_DB_URL} + ${env.LB_DB_USERNAME} + ${env.LB_DB_PASSWORD} + ${env.LB_CONTEXTS} + + diff --git a/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleCli.java b/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleCli.java index b8f0d4ab..78884a30 100644 --- a/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleCli.java +++ b/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleCli.java @@ -36,7 +36,8 @@ public class SampleCli *******************************************************************************/ public static void main(String[] args) { - new SampleCli().run(args); + int exitCode = new SampleCli().run(args); + System.exit(exitCode); } @@ -44,19 +45,19 @@ public class SampleCli /******************************************************************************* ** *******************************************************************************/ - private void run(String[] args) + int run(String[] args) { try { QInstance qInstance = SampleMetaDataProvider.defineInstance(); QPicoCliImplementation qPicoCliImplementation = new QPicoCliImplementation(qInstance); - int exitCode = qPicoCliImplementation.runCli("my-sample-cli", args); - System.exit(exitCode); + + return (qPicoCliImplementation.runCli("my-sample-cli", args)); } catch(Exception e) { e.printStackTrace(); - System.exit(-1); + return (-1); } } diff --git a/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleJavalinServer.java b/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleJavalinServer.java index aa5b049b..e88ec6c5 100644 --- a/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleJavalinServer.java +++ b/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleJavalinServer.java @@ -22,7 +22,6 @@ package com.kingsrook.sampleapp; -import com.kingsrook.qqq.backend.core.instances.QInstanceValidator; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.javalin.QJavalinImplementation; import io.javalin.Javalin; @@ -71,6 +70,24 @@ public class SampleJavalinServer config.enableCorsForAllOrigins(); }).start(PORT); javalinService.routes(qJavalinImplementation.getRoutes()); + + ///////////////////////////////////////////////////////////////// + // set the server to hot-swap the q instance before all routes // + ///////////////////////////////////////////////////////////////// + QJavalinImplementation.setQInstanceHotSwapSupplier(() -> + { + try + { + return (SampleMetaDataProvider.defineInstance()); + } + catch(Exception e) + { + LOG.warn("Error hot-swapping meta data", e); + return (null); + } + }); + javalinService.before(QJavalinImplementation::hotSwapQInstance); + javalinService.after(ctx -> ctx.res.setHeader("Access-Control-Allow-Origin", "http://localhost:3000")); } diff --git a/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleMetaDataProvider.java b/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleMetaDataProvider.java index 2c9bb956..7c0e5c00 100644 --- a/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleMetaDataProvider.java +++ b/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleMetaDataProvider.java @@ -253,7 +253,7 @@ public class SampleMetaDataProvider .withBackendName(RDBMS_BACKEND_NAME) .withPrimaryKeyField("id") .withRecordLabelFormat("%s %s") - .withRecordLabelFields(List.of("firstName", "lastName")) + .withRecordLabelFields("firstName", "lastName") .withField(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false)) .withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME).withBackendName("create_date").withIsEditable(false)) .withField(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME).withBackendName("modify_date").withIsEditable(false)) @@ -261,12 +261,13 @@ public class SampleMetaDataProvider .withField(new QFieldMetaData("lastName", QFieldType.STRING).withBackendName("last_name").withIsRequired(true)) .withField(new QFieldMetaData("birthDate", QFieldType.DATE).withBackendName("birth_date")) .withField(new QFieldMetaData("email", QFieldType.STRING)) + .withField(new QFieldMetaData("isEmployed", QFieldType.BOOLEAN).withBackendName("is_employed")) .withField(new QFieldMetaData("annualSalary", QFieldType.DECIMAL).withBackendName("annual_salary").withDisplayFormat(DisplayFormat.CURRENCY)) .withField(new QFieldMetaData("daysWorked", QFieldType.INTEGER).withBackendName("days_worked").withDisplayFormat(DisplayFormat.COMMAS)) .withSection(new QFieldSection("identity", "Identity", new QIcon("badge"), Tier.T1, List.of("id", "firstName", "lastName"))) .withSection(new QFieldSection("basicInfo", "Basic Info", new QIcon("dataset"), Tier.T2, List.of("email", "birthDate"))) - .withSection(new QFieldSection("employmentInfo", "Employment Info", new QIcon("work"), Tier.T2, List.of("annualSalary", "daysWorked"))) + .withSection(new QFieldSection("employmentInfo", "Employment Info", new QIcon("work"), Tier.T2, List.of("isEmployed", "annualSalary", "daysWorked"))) .withSection(new QFieldSection("dates", "Dates", new QIcon("calendar_month"), Tier.T3, List.of("createDate", "modifyDate"))); QInstanceEnricher.setInferredFieldBackendNames(qTableMetaData); diff --git a/qqq-sample-project/src/main/resources/liquibase/changelog.xml b/qqq-sample-project/src/main/resources/liquibase/changelog.xml new file mode 100644 index 00000000..9ceff1dd --- /dev/null +++ b/qqq-sample-project/src/main/resources/liquibase/changelog.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/qqq-sample-project/src/main/resources/liquibase/changesets/initial.xml b/qqq-sample-project/src/main/resources/liquibase/changesets/initial.xml new file mode 100644 index 00000000..4beba756 --- /dev/null +++ b/qqq-sample-project/src/main/resources/liquibase/changesets/initial.xml @@ -0,0 +1,100 @@ + + + + + + DROP TABLE IF EXISTS person; + CREATE TABLE person + ( + id INT AUTO_INCREMENT primary key , + create_date TIMESTAMP DEFAULT now(), + modify_date TIMESTAMP DEFAULT now(), + + first_name VARCHAR(80) NOT NULL, + last_name VARCHAR(80) NOT NULL, + birth_date DATE, + email VARCHAR(250) NOT NULL, + is_employed BOOLEAN, + annual_salary DECIMAL(12,2), + days_worked INTEGER + ); + INSERT INTO person (id, first_name, last_name, birth_date, email, is_employed, annual_salary, days_worked) VALUES (1, 'Darin', 'Kelkhoff', '1980-05-31', 'darin.kelkhoff@gmail.com', 1, 25000, 27); + INSERT INTO person (id, first_name, last_name, birth_date, email, is_employed, annual_salary, days_worked) VALUES (2, 'James', 'Maes', '1980-05-15', 'jmaes@mmltholdings.com', 1, 26000, 124); + INSERT INTO person (id, first_name, last_name, birth_date, email, is_employed, annual_salary, days_worked) VALUES (3, 'Tim', 'Chamberlain', '1976-05-28', 'tchamberlain@mmltholdings.com', 0, null, 0); + INSERT INTO person (id, first_name, last_name, birth_date, email, is_employed, annual_salary, days_worked) VALUES (4, 'Tyler', 'Samples', NULL, 'tsamples@mmltholdings.com', 1, 30000, 99); + INSERT INTO person (id, first_name, last_name, birth_date, email, is_employed, annual_salary, days_worked) VALUES (5, 'Garret', 'Richardson', '1981-01-01', 'grichardson@mmltholdings.com', 1, 1000000, 232); + + + DROP TABLE IF EXISTS carrier; + CREATE TABLE carrier + ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(80) NOT NULL, + company_code VARCHAR(80) NOT NULL, + service_level VARCHAR(80) NOT NULL + ); + INSERT INTO carrier (id, name, company_code, service_level) VALUES (1, 'UPS Ground', 'UPS', 'G'); + INSERT INTO carrier (id, name, company_code, service_level) VALUES (2, 'UPS 2Day', 'UPS', '2'); + INSERT INTO carrier (id, name, company_code, service_level) VALUES (3, 'UPS International', 'UPS', 'I'); + INSERT INTO carrier (id, name, company_code, service_level) VALUES (4, 'Fedex Ground', 'FEDEX', 'G'); + INSERT INTO carrier (id, name, company_code, service_level) VALUES (5, 'Fedex Next Day', 'UPS', '1'); + INSERT INTO carrier (id, name, company_code, service_level) VALUES (6, 'Will Call', 'WILL_CALL', 'W'); + INSERT INTO carrier (id, name, company_code, service_level) VALUES (7, 'USPS Priority', 'USPS', '1'); + INSERT INTO carrier (id, name, company_code, service_level) VALUES (8, 'USPS Super Slow', 'USPS', '4'); + INSERT INTO carrier (id, name, company_code, service_level) VALUES (9, 'USPS Super Fast', 'USPS', '0'); + INSERT INTO carrier (id, name, company_code, service_level) VALUES (10, 'DHL International', 'DHL', 'I'); + INSERT INTO carrier (id, name, company_code, service_level) VALUES (11, 'GSO', 'GSO', 'G'); + + + DROP TABLE IF EXISTS child_table; + CREATE TABLE child_table + ( + id INT AUTO_INCREMENT primary key, + name VARCHAR(80) NOT NULL + ); + INSERT INTO child_table (id, name) VALUES (1, 'Timmy'); + INSERT INTO child_table (id, name) VALUES (2, 'Jimmy'); + INSERT INTO child_table (id, name) VALUES (3, 'Johnny'); + INSERT INTO child_table (id, name) VALUES (4, 'Gracie'); + INSERT INTO child_table (id, name) VALUES (5, 'Suzie'); + + + DROP TABLE IF EXISTS parent_table; + CREATE TABLE parent_table + ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(80) NOT NULL, + child_id INT, + foreign key (child_id) references child_table(id) + ); + INSERT INTO parent_table (id, name, child_id) VALUES (1, 'Tim''s Dad', 1); + INSERT INTO parent_table (id, name, child_id) VALUES (2, 'Tim''s Mom', 1); + INSERT INTO parent_table (id, name, child_id) VALUES (3, 'Childless Man', null); + INSERT INTO parent_table (id, name, child_id) VALUES (4, 'Childless Woman', null); + INSERT INTO parent_table (id, name, child_id) VALUES (5, 'Johny''s Single Dad', 3); + + + DROP TABLE IF EXISTS city; + CREATE TABLE city + ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(80) NOT NULL, + state VARCHAR(2) NOT NULL + ); + INSERT INTO city (id, name, state) VALUES (1, 'Decatur', 'IL'); + INSERT INTO city (id, name, state) VALUES (2, 'Chester', 'IL'); + INSERT INTO city (id, name, state) VALUES (3, 'St. Louis', 'MO'); + INSERT INTO city (id, name, state) VALUES (4, 'Baltimore', 'MD'); + INSERT INTO city (id, name, state) VALUES (5, 'New York', 'NY'); + + + + + diff --git a/qqq-sample-project/src/main/resources/liquibase/liquibase.properties b/qqq-sample-project/src/main/resources/liquibase/liquibase.properties new file mode 100644 index 00000000..160d8648 --- /dev/null +++ b/qqq-sample-project/src/main/resources/liquibase/liquibase.properties @@ -0,0 +1,6 @@ +#liquibase.properties +classpath: /src/main/resources/liquibase/lib/mysql-connector-java-8.0.29.jar +driver: com.mysql.cj.jdbc.Driver +changeLogFile:/src/main/resources/liquibase/changelog.xml +logLevel: INFO +liquibase.hub.mode=off \ No newline at end of file diff --git a/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/SampleCliTest.java b/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/SampleCliTest.java new file mode 100644 index 00000000..be51bead --- /dev/null +++ b/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/SampleCliTest.java @@ -0,0 +1,58 @@ +/* + * 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.sampleapp; + + +import com.kingsrook.qqq.backend.core.exceptions.QException; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + + +/******************************************************************************* + ** Unit test for SampleCli + *******************************************************************************/ +class SampleCliTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testExitSuccess() throws QException + { + int exitCode = new SampleCli().run(new String[] { "--meta-data" }); + assertEquals(0, exitCode); + } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testNotExitSuccess() throws QException + { + int exitCode = new SampleCli().run(new String[] { "asdfasdf" }); + assertNotEquals(0, exitCode); + } + +} \ No newline at end of file diff --git a/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/SampleJavalinServerTest.java b/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/SampleJavalinServerTest.java index 06bcd1e7..0a861b18 100644 --- a/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/SampleJavalinServerTest.java +++ b/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/SampleJavalinServerTest.java @@ -2,14 +2,14 @@ package com.kingsrook.sampleapp; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.*; /******************************************************************************* - ** Unit test for com.kingsrook.sampleapp.SampleJavalinServer + ** Unit test for SampleJavalinServer *******************************************************************************/ class SampleJavalinServerTest { + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/SampleMetaDataProviderTest.java b/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/SampleMetaDataProviderTest.java index 6192bb38..08b5c6ea 100644 --- a/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/SampleMetaDataProviderTest.java +++ b/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/SampleMetaDataProviderTest.java @@ -43,7 +43,6 @@ import com.kingsrook.qqq.backend.module.filesystem.local.actions.FilesystemQuery import com.kingsrook.qqq.backend.module.filesystem.local.model.metadata.FilesystemTableBackendDetails; import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager; import com.kingsrook.qqq.backend.module.rdbms.jdbc.QueryManager; -import junit.framework.Assert; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.junit.jupiter.api.Assertions; @@ -84,7 +83,7 @@ class SampleMetaDataProviderTest try(Connection connection = connectionManager.getConnection(SampleMetaDataProvider.defineRdbmsBackend())) { InputStream primeTestDatabaseSqlStream = SampleMetaDataProviderTest.class.getResourceAsStream("/" + sqlFileName); - Assert.assertNotNull(primeTestDatabaseSqlStream); + assertNotNull(primeTestDatabaseSqlStream); List lines = (List) IOUtils.readLines(primeTestDatabaseSqlStream); lines = lines.stream().filter(line -> !line.startsWith("-- ")).toList(); String joinedSQL = String.join("\n", lines); diff --git a/qqq-sample-project/src/test/resources/prime-test-database.sql b/qqq-sample-project/src/test/resources/prime-test-database.sql index ef295c31..10185156 100644 --- a/qqq-sample-project/src/test/resources/prime-test-database.sql +++ b/qqq-sample-project/src/test/resources/prime-test-database.sql @@ -31,15 +31,16 @@ CREATE TABLE person birth_date DATE, email VARCHAR(250) NOT NULL, + is_employed BOOLEAN, annual_salary DECIMAL(12, 2), days_worked INTEGER ); -INSERT INTO person (id, first_name, last_name, birth_date, email, annual_salary, days_worked) VALUES (1, 'Darin', 'Kelkhoff', '1980-05-31', 'darin.kelkhoff@gmail.com', 75003.50, 1001); -INSERT INTO person (id, first_name, last_name, birth_date, email, annual_salary, days_worked) VALUES (2, 'James', 'Maes', '1980-05-15', 'jmaes@mmltholdings.com', 150000, 10100); -INSERT INTO person (id, first_name, last_name, birth_date, email, annual_salary, days_worked) VALUES (3, 'Tim', 'Chamberlain', '1976-05-28', 'tchamberlain@mmltholdings.com', 300000, 100100); -INSERT INTO person (id, first_name, last_name, birth_date, email, annual_salary, days_worked) VALUES (4, 'Tyler', 'Samples', NULL, 'tsamples@mmltholdings.com', 950000, 75); -INSERT INTO person (id, first_name, last_name, birth_date, email, annual_salary, days_worked) VALUES (5, 'Garret', 'Richardson', '1981-01-01', 'grichardson@mmltholdings.com', 1500000, 1); +INSERT INTO person (id, first_name, last_name, birth_date, email, is_employed, annual_salary, days_worked) VALUES (1, 'Darin', 'Kelkhoff', '1980-05-31', 'darin.kelkhoff@gmail.com', 1, 75003.50, 1001); +INSERT INTO person (id, first_name, last_name, birth_date, email, is_employed, annual_salary, days_worked) VALUES (2, 'James', 'Maes', '1980-05-15', 'jmaes@mmltholdings.com', 1, 150000, 10100); +INSERT INTO person (id, first_name, last_name, birth_date, email, is_employed, annual_salary, days_worked) VALUES (3, 'Tim', 'Chamberlain', '1976-05-28', 'tchamberlain@mmltholdings.com', 1, 300000, 100100); +INSERT INTO person (id, first_name, last_name, birth_date, email, is_employed, annual_salary, days_worked) VALUES (4, 'Tyler', 'Samples', NULL, 'tsamples@mmltholdings.com', 1, 950000, 75); +INSERT INTO person (id, first_name, last_name, birth_date, email, is_employed, annual_salary, days_worked) VALUES (5, 'Garret', 'Richardson', '1981-01-01', 'grichardson@mmltholdings.com', 0, 1500000, 1); DROP TABLE IF EXISTS carrier; CREATE TABLE carrier