Merge branch 'feature/sprint-9-support-updates' of github.com:Kingsrook/qqq into feature/sprint-9-support-updates

This commit is contained in:
Tim Chamberlain
2022-08-19 09:57:36 -05:00
41 changed files with 1878 additions and 119 deletions

View File

@ -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);

View File

@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.actions.tables;
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
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;
@ -45,14 +46,24 @@ public class QueryAction
ActionHelper.validateSession(queryInput);
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());
if(queryInput.getShouldGenerateDisplayValues())
{
QValueFormatter qValueFormatter = new QValueFormatter();
qValueFormatter.setDisplayValuesInRecords(queryInput.getTable(), queryOutput.getRecords());
}
if(queryInput.getShouldTranslatePossibleValues())
{
QPossibleValueTranslator qPossibleValueTranslator = new QPossibleValueTranslator(queryInput.getInstance(), queryInput.getSession());
qPossibleValueTranslator.translatePossibleValuesInRecords(queryInput.getTable(), queryOutput.getRecords());
}
}
return queryOutput;

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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
}

View File

@ -0,0 +1,359 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
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.tables.QueryAction;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
import com.kingsrook.qqq.backend.core.model.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<String, Map<Serializable, String>> 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<QRecord> 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);
}
// todo - memoize!!!
// todo - bulk!!!
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 translatePossibleValueCustom(QFieldMetaData field, Serializable value, QPossibleValueSource possibleValueSource)
{
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 (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 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<Serializable, String> cacheForPvs = possibleValueCache.get(possibleValueSource.getName());
if(!cacheForPvs.containsKey(value))
{
primePvsCache(possibleValueSource.getTableName(), List.of(possibleValueSource), List.of(value));
}
return (cacheForPvs.get(value));
}
/*******************************************************************************
**
*******************************************************************************/
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<String> valueFields, Object id, String label)
{
List<Object> 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()));
}
/*******************************************************************************
**
*******************************************************************************/
private String translatePossibleValueEnum(Serializable value, QPossibleValueSource possibleValueSource)
{
for(QPossibleValue<?> possibleValue : possibleValueSource.getEnumValues())
{
if(possibleValue.getId().equals(value))
{
return (formatPossibleValue(possibleValueSource, possibleValue));
}
}
return (null);
}
/*******************************************************************************
** prime the cache (e.g., by doing bulk-queries) for table-based PVS's
*******************************************************************************/
void primePvsCache(QTableMetaData table, List<QRecord> records)
{
ListingHash<String, QFieldMetaData> fieldsByPvsTable = new ListingHash<>();
ListingHash<String, QPossibleValueSource> 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<Serializable> 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<QPossibleValueSource> possibleValueSources, Collection<Serializable> values)
{
for(QPossibleValueSource possibleValueSource : possibleValueSources)
{
possibleValueCache.putIfAbsent(possibleValueSource.getName(), new HashMap<>());
}
try
{
String primaryKeyField = qInstance.getTable(tableName).getPrimaryKeyField();
for(List<Serializable> 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);
}
}
}

View File

@ -25,8 +25,10 @@ package com.kingsrook.qqq.backend.core.actions.values;
import java.io.Serializable;
import java.util.List;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import org.apache.logging.log4j.LogManager;
@ -34,7 +36,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.
** Note that this includes handling PossibleValues.
*******************************************************************************/
public class QValueFormatter
{
@ -45,7 +48,16 @@ public class QValueFormatter
/*******************************************************************************
**
*******************************************************************************/
public static String formatValue(QFieldMetaData field, Serializable value)
public QValueFormatter()
{
}
/*******************************************************************************
**
*******************************************************************************/
public String formatValue(QFieldMetaData field, Serializable value)
{
//////////////////////////////////
// null values get null results //
@ -55,6 +67,16 @@ public class QValueFormatter
return (null);
}
// todo - is this appropriate, with this class and possibleValueTransaltor being decoupled - to still do standard formatting here?
// alternatively, shold we return null here?
// ///////////////////////////////////////////////
// // if the field has a possible value, use it //
// ///////////////////////////////////////////////
// if(field.getPossibleValueSourceName() != null)
// {
// return (this.possibleValueTranslator.translatePossibleValue(field, value));
// }
////////////////////////////////////////////////////////
// if the field has a display format, try to apply it //
////////////////////////////////////////////////////////
@ -68,6 +90,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 +122,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 +151,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 +179,7 @@ public class QValueFormatter
/*******************************************************************************
** For a list of records, set their recordLabels and display values
*******************************************************************************/
public static void setDisplayValuesInRecords(QTableMetaData table, List<QRecord> records)
public void setDisplayValuesInRecords(QTableMetaData table, List<QRecord> records)
{
if(records == null)
{
@ -167,12 +190,13 @@ 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));
}
}
}

View File

@ -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<String, String> 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));
}

View File

@ -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,7 @@ 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"));
return (name.substring(0, 1).toUpperCase(Locale.ROOT) + name.substring(1).replaceAll("([A-Z0-9]+)", " $1").replaceAll("([0-9])([A-Za-z])", "$1 $2"));
}
@ -579,7 +585,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);

View File

@ -29,6 +29,7 @@ import java.util.Objects;
import java.util.Set;
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.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;
@ -88,6 +89,7 @@ public class QInstanceValidator
validateTables(qInstance, errors);
validateProcesses(qInstance, errors);
validateApps(qInstance, errors);
validatePossibleValueSources(qInstance, errors);
}
catch(Exception e)
{
@ -167,8 +169,8 @@ public class QInstanceValidator
//////////////////////////////////////////
// validate field sections in the table //
//////////////////////////////////////////
Set<String> fieldNamesInSections = new HashSet<>();
QFieldSection tier1Section = null;
Set<String> fieldNamesInSections = new HashSet<>();
QFieldSection tier1Section = null;
if(table.getSections() != null)
{
for(QFieldSection section : table.getSections())
@ -190,6 +192,16 @@ 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.");
}
}
});
}
}
@ -225,7 +237,7 @@ public class QInstanceValidator
*******************************************************************************/
private void validateProcesses(QInstance qInstance, List<String> errors)
{
if(!CollectionUtils.nullSafeIsEmpty(qInstance.getProcesses()))
if(CollectionUtils.nullSafeHasContents(qInstance.getProcesses()))
{
qInstance.getProcesses().forEach((processName, process) ->
{
@ -264,7 +276,7 @@ public class QInstanceValidator
*******************************************************************************/
private void validateApps(QInstance qInstance, List<String> errors)
{
if(!CollectionUtils.nullSafeIsEmpty(qInstance.getApps()))
if(CollectionUtils.nullSafeHasContents(qInstance.getApps()))
{
qInstance.getApps().forEach((appName, app) ->
{
@ -291,6 +303,61 @@ public class QInstanceValidator
/*******************************************************************************
**
*******************************************************************************/
private void validatePossibleValueSources(QInstance qInstance, List<String> 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() + ".");
assertCondition(errors, possibleValueSource.getIdType() != null, "Missing an idType for possibleValueSource: " + pvsName);
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.");
}
}
default -> errors.add("Unexpected possibleValueSource type: " + possibleValueSource.getType());
}
}
});
}
}
/*******************************************************************************
** Check if an app's child list can recursively be traversed without finding a
** duplicate, which would indicate a cycle (e.g., an error)

View File

@ -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;
}
}

View File

@ -62,6 +62,11 @@ public @interface QField
*******************************************************************************/
String displayFormat() default "";
/*******************************************************************************
**
*******************************************************************************/
String possibleValueSourceName() default "";
//////////////////////////////////////////////////////////////////////////////////////////
// new attributes here likely need implementation in QFieldMetaData.constructFromGetter //
//////////////////////////////////////////////////////////////////////////////////////////

View File

@ -54,10 +54,10 @@ public class QInstance
////////////////////////////////////////////////////////////////////////////////////////////
// Important to use LinkedHashmap here, to preserve the order in which entries are added. //
////////////////////////////////////////////////////////////////////////////////////////////
private Map<String, QTableMetaData> tables = new LinkedHashMap<>();
private Map<String, QPossibleValueSource<?>> possibleValueSources = new LinkedHashMap<>();
private Map<String, QProcessMetaData> processes = new LinkedHashMap<>();
private Map<String, QAppMetaData> apps = new LinkedHashMap<>();
private Map<String, QTableMetaData> tables = new LinkedHashMap<>();
private Map<String, QPossibleValueSource> possibleValueSources = new LinkedHashMap<>();
private Map<String, QProcessMetaData> processes = new LinkedHashMap<>();
private Map<String, QAppMetaData> 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<String, QPossibleValueSource<?>> getPossibleValueSources()
public Map<String, QPossibleValueSource> getPossibleValueSources()
{
return possibleValueSources;
}
@ -364,7 +364,7 @@ public class QInstance
** Setter for possibleValueSources
**
*******************************************************************************/
public void setPossibleValueSources(Map<String, QPossibleValueSource<?>> possibleValueSources)
public void setPossibleValueSources(Map<String, QPossibleValueSource> possibleValueSources)
{
this.possibleValueSources = possibleValueSources;
}

View File

@ -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()));

View File

@ -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
}

View File

@ -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
**

View File

@ -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;
}
}

View File

@ -0,0 +1,39 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.metadata.possiblevalues;
/*******************************************************************************
**
*******************************************************************************/
public interface PossibleValueEnum<T>
{
/*******************************************************************************
**
*******************************************************************************/
T getPossibleValueId();
/*******************************************************************************
**
*******************************************************************************/
String getPossibleValueLabel();
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.metadata.possiblevalues;
/*******************************************************************************
** An actual possible value - an id and label.
**
*******************************************************************************/
public class QPossibleValue<T>
{
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;
}
}

View File

@ -24,19 +24,62 @@ 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;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
/*******************************************************************************
** Meta-data to represent a single field in a table.
**
*******************************************************************************/
public class QPossibleValueSource<T>
public class QPossibleValueSource
{
private String name;
private String name;
private QPossibleValueSourceType type;
private QFieldType idType = QFieldType.INTEGER;
// should these be in sub-types??
private List<T> enumValues;
private String valueFormat = ValueFormat.DEFAULT;
private List<String> valueFields = ValueFields.DEFAULT;
private String valueFormatIfNotFound = null;
private List<String> valueFieldsIfNotFound = null;
public interface ValueFormat
{
String DEFAULT = "%s";
String LABEL_ONLY = "%s";
String LABEL_PARENS_ID = "%s (%s)";
String ID_COLON_LABEL = "%s: %s";
}
public interface ValueFields
{
List<String> DEFAULT = List.of("label");
List<String> LABEL_ONLY = List.of("label");
List<String> LABEL_PARENS_ID = List.of("label", "id");
List<String> ID_COLON_LABEL = List.of("id", "label");
}
// 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<QPossibleValue<?>> enumValues;
///////////////////////
// for type = CUSTOM //
///////////////////////
private QCodeReference customCodeReference;
@ -72,7 +115,7 @@ public class QPossibleValueSource<T>
/*******************************************************************************
**
*******************************************************************************/
public QPossibleValueSource<T> withName(String name)
public QPossibleValueSource withName(String name)
{
this.name = name;
return (this);
@ -103,7 +146,7 @@ public class QPossibleValueSource<T>
/*******************************************************************************
**
*******************************************************************************/
public QPossibleValueSource<T> withType(QPossibleValueSourceType type)
public QPossibleValueSource withType(QPossibleValueSourceType type)
{
this.type = type;
return (this);
@ -111,11 +154,215 @@ public class QPossibleValueSource<T>
/*******************************************************************************
** Getter for idType
**
*******************************************************************************/
public QFieldType getIdType()
{
return idType;
}
/*******************************************************************************
** Setter for idType
**
*******************************************************************************/
public void setIdType(QFieldType idType)
{
this.idType = idType;
}
/*******************************************************************************
** Fluent setter for idType
**
*******************************************************************************/
public QPossibleValueSource withIdType(QFieldType idType)
{
this.idType = idType;
return (this);
}
/*******************************************************************************
** 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<String> getValueFields()
{
return valueFields;
}
/*******************************************************************************
** Setter for valueFields
**
*******************************************************************************/
public void setValueFields(List<String> valueFields)
{
this.valueFields = valueFields;
}
/*******************************************************************************
** Fluent setter for valueFields
**
*******************************************************************************/
public QPossibleValueSource withValueFields(List<String> 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<String> getValueFieldsIfNotFound()
{
return valueFieldsIfNotFound;
}
/*******************************************************************************
** Setter for valueFieldsIfNotFound
**
*******************************************************************************/
public void setValueFieldsIfNotFound(List<String> valueFieldsIfNotFound)
{
this.valueFieldsIfNotFound = valueFieldsIfNotFound;
}
/*******************************************************************************
** Fluent setter for valueFieldsIfNotFound
**
*******************************************************************************/
public QPossibleValueSource withValueFieldsIfNotFound(List<String> 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<T> getEnumValues()
public List<QPossibleValue<?>> getEnumValues()
{
return enumValues;
}
@ -126,7 +373,7 @@ public class QPossibleValueSource<T>
** Setter for enumValues
**
*******************************************************************************/
public void setEnumValues(List<T> enumValues)
public void setEnumValues(List<QPossibleValue<?>> enumValues)
{
this.enumValues = enumValues;
}
@ -137,7 +384,7 @@ public class QPossibleValueSource<T>
** Fluent setter for enumValues
**
*******************************************************************************/
public QPossibleValueSource<T> withEnumValues(List<T> enumValues)
public QPossibleValueSource withEnumValues(List<QPossibleValue<?>> enumValues)
{
this.enumValues = enumValues;
return this;
@ -146,16 +393,64 @@ public class QPossibleValueSource<T>
/*******************************************************************************
** Fluent adder for enumValues
**
*******************************************************************************/
public QPossibleValueSource<T> 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);
}
/*******************************************************************************
**
*******************************************************************************/
public <T extends PossibleValueEnum<?>> 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);
}
}

View File

@ -24,6 +24,7 @@ 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;
@ -587,6 +588,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
**

View File

@ -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<Instant> lastTimeCheckedOptional = spi.get(Instant.class, key);

View File

@ -24,18 +24,21 @@ 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;
/*******************************************************************************
@ -48,6 +51,12 @@ public class MemoryRecordStore
private Map<String, Map<Serializable, QRecord>> data;
private Map<String, Integer> nextSerials;
private static boolean collectStatistics = false;
private static final Map<String, Integer> statistics = Collections.synchronizedMap(new HashMap<>());
public static final String STAT_QUERIES_RAN = "queriesRan";
/*******************************************************************************
@ -105,9 +114,56 @@ public class MemoryRecordStore
*******************************************************************************/
public List<QRecord> query(QueryInput input)
{
incrementStatistic(STAT_QUERIES_RAN);
Map<Serializable, QRecord> tableData = getTableData(input.getTable());
List<QRecord> records = new ArrayList<>(tableData.values());
// todo - filtering
List<QRecord> 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);
}
@ -120,7 +176,7 @@ public class MemoryRecordStore
{
Map<Serializable, QRecord> tableData = getTableData(input.getTable());
List<QRecord> records = new ArrayList<>(tableData.values());
// todo - filtering
// todo - filtering (call query)
return (records.size());
}
@ -235,4 +291,53 @@ public class MemoryRecordStore
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<String, Integer> getStatistics()
{
return statistics;
}
}

View File

@ -82,6 +82,8 @@ public class BasicETLLoadAsUpdateFunction implements BackendStep
for(List<QRecord> 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);

View File

@ -86,6 +86,8 @@ public class BasicETLLoadFunction implements BackendStep
for(List<QRecord> 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);

View File

@ -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
{

View File

@ -47,18 +47,33 @@ 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();
}
}
}

View File

@ -0,0 +1,241 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
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.actions.tables.InsertAction;
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.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
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.possiblevalues.QPossibleValueSource;
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.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
/*******************************************************************************
**
*******************************************************************************/
public class QPossibleValueTranslatorTest
{
/*******************************************************************************
**
*******************************************************************************/
@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.setValueFormat(QPossibleValueSource.ValueFormat.LABEL_ONLY);
possibleValueSource.setValueFields(QPossibleValueSource.ValueFields.LABEL_ONLY);
assertEquals("IL", possibleValueTranslator.translatePossibleValue(stateField, 1));
///////////////////////////////////////
// assert the LABEL_PARAMS_ID format //
///////////////////////////////////////
possibleValueSource.setValueFormat(QPossibleValueSource.ValueFormat.LABEL_PARENS_ID);
possibleValueSource.setValueFields(QPossibleValueSource.ValueFields.LABEL_PARENS_ID);
assertEquals("IL (1)", possibleValueTranslator.translatePossibleValue(stateField, 1));
//////////////////////////////////////
// assert the ID_COLON_LABEL format //
//////////////////////////////////////
possibleValueSource.setValueFormat(QPossibleValueSource.ValueFormat.ID_COLON_LABEL);
possibleValueSource.setValueFields(QPossibleValueSource.ValueFields.ID_COLON_LABEL);
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());
List<QRecord> shapeRecords = List.of(
new QRecord().withTableName(shapeTable.getName()).withValue("id", 1).withValue("name", "Triangle"),
new QRecord().withTableName(shapeTable.getName()).withValue("id", 2).withValue("name", "Square"),
new QRecord().withTableName(shapeTable.getName()).withValue("id", 3).withValue("name", "Circle"));
InsertInput insertInput = new InsertInput(qInstance);
insertInput.setSession(new QSession());
insertInput.setTableName(shapeTable.getName());
insertInput.setRecords(shapeRecords);
new InsertAction().execute(insertInput);
//////////////////////////////////////////////////////////////////////////
// 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.setValueFormat(QPossibleValueSource.ValueFormat.LABEL_PARENS_ID);
possibleValueSource.setValueFields(QPossibleValueSource.ValueFields.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<QRecord> 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");
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testSetDisplayValuesInRecords()
{
QTableMetaData table = new QTableMetaData()
.withRecordLabelFormat("%s %s")
.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("homeStateId", QFieldType.INTEGER).withPossibleValueSourceName(TestUtils.POSSIBLE_VALUE_SOURCE_STATE));
/////////////////////////////////////////////////////////////////
// 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<QRecord> 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"));
}
}

View File

@ -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<QRecord> 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.
}
}

View File

@ -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,60 @@ 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();
List<QRecord> 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<QRecord> 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<QRecord> 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"));
}
}

View File

@ -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,20 @@ 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"));
}
/*******************************************************************************
**
*******************************************************************************/
@ -146,4 +160,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());
}
}

View File

@ -22,16 +22,20 @@
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 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.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;
@ -110,7 +114,7 @@ class QInstanceValidatorTest
@Test
public void test_validateNullTables()
{
assertValidationFailureReasons((qInstance) ->
assertValidationFailureReasonsAllowingExtraReasons((qInstance) ->
{
qInstance.setTables(null);
qInstance.setProcesses(null);
@ -127,7 +131,7 @@ class QInstanceValidatorTest
@Test
public void test_validateEmptyTables()
{
assertValidationFailureReasons((qInstance) ->
assertValidationFailureReasonsAllowingExtraReasons((qInstance) ->
{
qInstance.setTables(new HashMap<>());
qInstance.setProcesses(new HashMap<>());
@ -150,10 +154,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 +191,19 @@ 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.
**
@ -252,7 +272,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 +339,7 @@ class QInstanceValidatorTest
}
/*******************************************************************************
**
*******************************************************************************/
@ -376,6 +397,7 @@ class QInstanceValidatorTest
}
/*******************************************************************************
**
*******************************************************************************/
@ -391,6 +413,7 @@ class QInstanceValidatorTest
}
/*******************************************************************************
**
*******************************************************************************/
@ -408,6 +431,96 @@ class QInstanceValidatorTest
/*******************************************************************************
**
*******************************************************************************/
@Test
void testPossibleValueSourceMissingType()
{
assertValidationFailureReasons((qInstance) -> qInstance.getPossibleValueSource(TestUtils.POSSIBLE_VALUE_SOURCE_STATE).setType(null),
"Missing type for possibleValueSource");
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testPossibleValueSourceMissingIdType()
{
assertValidationFailureReasons((qInstance) -> qInstance.getPossibleValueSource(TestUtils.POSSIBLE_VALUE_SOURCE_STATE).setIdType(null),
"Missing an idType 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");
}
/*******************************************************************************
** 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

View File

@ -48,6 +48,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.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;
@ -62,8 +63,9 @@ class MemoryBackendModuleTest
/*******************************************************************************
**
*******************************************************************************/
@BeforeEach
@AfterEach
void afterEach()
void beforeAndAfter()
{
MemoryRecordStore.getInstance().reset();
}
@ -120,6 +122,8 @@ class MemoryBackendModuleTest
assertEquals(3, new CountAction().execute(countInput).getCount());
// todo - filters in query
//////////////////
// do an update //
//////////////////

View File

@ -22,10 +22,12 @@
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.QueryAction;
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.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
@ -39,6 +41,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;
@ -80,6 +83,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
/*******************************************************************************
@ -99,6 +106,8 @@ public class TestUtils
qInstance.addTable(defineTableShape());
qInstance.addPossibleValueSource(defineStatesPossibleValueSource());
qInstance.addPossibleValueSource(defineShapePossibleValueSource());
qInstance.addPossibleValueSource(defineCustomPossibleValueSource());
qInstance.addProcess(defineProcessGreetPeople());
qInstance.addProcess(defineProcessGreetPeopleInteractive());
@ -141,12 +150,40 @@ public class TestUtils
** Define the "states" possible value source used in standard tests
**
*******************************************************************************/
private static QPossibleValueSource<String> defineStatesPossibleValueSource()
private static QPossibleValueSource defineStatesPossibleValueSource()
{
return new QPossibleValueSource<String>()
.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));
}
@ -205,7 +242,10 @@ 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))
;
}
@ -219,6 +259,7 @@ public class TestUtils
.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))
@ -452,4 +493,21 @@ public class TestUtils
""");
}
/*******************************************************************************
**
*******************************************************************************/
public static class CustomPossibleValueSource implements QCustomPossibleValueProvider
{
/*******************************************************************************
**
*******************************************************************************/
@Override
public QPossibleValue<?> getPossibleValue(Serializable idValue)
{
return (new QPossibleValue<>(idValue, "Custom[" + idValue + "]"));
}
}
}

View File

@ -60,8 +60,8 @@
"type": "STRING",
"possibleValueSourceName": null
},
"homeState": {
"name": "homeState",
"homeStateId": {
"name": "homeStateId",
"label": null,
"backendName": null,
"type": "STRING",

View File

@ -27,8 +27,8 @@
"type": "DATE_TIME",
"possibleValueSourceName": null
},
"homeState": {
"name": "homeState",
"homeStateId": {
"name": "homeStateId",
"backendName": null,
"label": null,
"type": "STRING",