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

@ -261,7 +261,7 @@ echo "------------------------------------------------------------"
which xpath > /dev/null 2>&1
if [ "$?" == "0" ]; then
echo "Element\nInstructions Missed\nInstruction Coverage\nBranches Missed\nBranch Coverage\nComplexity Missed\nComplexity Hit\nLines Missed\nLines Hit\nMethods Missed\nMethods Hit\nClasses Missed\nClasses Hit\n" > /tmp/$$.headers
xpath -q -e '/html/body/table/tfoot/tr[1]/td/text()' target/site/jacoco/index.html > /tmp/$$.values
xpath -n -q -e '/html/body/table/tfoot/tr[1]/td/text()' target/site/jacoco/index.html > /tmp/$$.values
paste /tmp/$$.headers /tmp/$$.values | tail +2 | awk -v FS='\t' '{printf("%-20s %s\n",$1,$2)}'
rm /tmp/$$.headers /tmp/$$.values
else

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;
@ -50,9 +51,19 @@ public class QueryAction
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)
{
@ -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

@ -55,7 +55,7 @@ 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, QPossibleValueSource> possibleValueSources = new LinkedHashMap<>();
private Map<String, QProcessMetaData> processes = new LinkedHashMap<>();
private Map<String, QAppMetaData> apps = new LinkedHashMap<>();
@ -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

@ -41,6 +41,8 @@ public class QFrontendFieldMetaData
private QFieldType type;
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 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",

View File

@ -200,6 +200,10 @@ public class RDBMSQueryAction extends AbstractRDBMSAction implements QueryInterf
{
return (QueryManager.getLocalDateTime(resultSet, i));
}
case BOOLEAN:
{
return (QueryManager.getBoolean(resultSet, i));
}
default:
{
throw new IllegalStateException("Unexpected field type: " + qFieldMetaData.getType());

View File

@ -428,12 +428,14 @@ public class RDBMSQueryActionTest extends RDBMSActionTest
/*******************************************************************************
** This doesn't really test any RDBMS code, but is a checkpoint that the core
** module is populating displayValues when it performs the system-level query action.
** module is populating displayValues when it performs the system-level query action
** (if so requested by input field).
*******************************************************************************/
@Test
public void testThatDisplayValuesGetSetGoingThroughQueryAction() throws QException
{
QueryInput queryInput = initQueryRequest();
queryInput.setShouldGenerateDisplayValues(true);
QueryOutput queryOutput = new QueryAction().execute(queryInput);
Assertions.assertEquals(5, queryOutput.getRecords().size(), "Unfiltered query should find all rows");

View File

@ -34,6 +34,7 @@ import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Supplier;
import com.kingsrook.qqq.backend.core.actions.async.AsyncJobManager;
import com.kingsrook.qqq.backend.core.actions.metadata.MetaDataAction;
import com.kingsrook.qqq.backend.core.actions.metadata.ProcessMetaDataAction;
@ -114,6 +115,11 @@ public class QJavalinImplementation
static QInstance qInstance;
private static Supplier<QInstance> qInstanceHotSwapSupplier;
private static long lastQInstanceHotSwapMillis;
private static final long MILLIS_BETWEEN_HOT_SWAPS = 2500;
private static int DEFAULT_PORT = 8001;
private static Javalin service;
@ -166,6 +172,44 @@ public class QJavalinImplementation
// todo base path from arg? - and then potentially multiple instances too (chosen based on the root path??)
service = Javalin.create().start(port);
service.routes(getRoutes());
service.before(QJavalinImplementation::hotSwapQInstance);
}
/*******************************************************************************
** If there's a qInstanceHotSwapSupplier, and its been a little while, replace
** the qInstance with a new one from the supplier. Meant to be used while doing
** development.
*******************************************************************************/
public static void hotSwapQInstance(Context context)
{
if(qInstanceHotSwapSupplier != null)
{
long now = System.currentTimeMillis();
if(now - lastQInstanceHotSwapMillis < MILLIS_BETWEEN_HOT_SWAPS)
{
return;
}
lastQInstanceHotSwapMillis = now;
try
{
QInstance newQInstance = qInstanceHotSwapSupplier.get();
new QInstanceValidator().validate(newQInstance);
QJavalinImplementation.qInstance = newQInstance;
LOG.info("Swapped qInstance");
}
catch(QInstanceValidationException e)
{
LOG.warn(e.getMessage());
}
catch(Exception e)
{
LOG.error("Error swapping QInstance", e);
}
}
}
@ -266,7 +310,7 @@ public class QJavalinImplementation
else
{
String authorizationHeaderValue = context.header("Authorization");
if (authorizationHeaderValue != null)
if(authorizationHeaderValue != null)
{
String bearerPrefix = "Bearer ";
if(authorizationHeaderValue.startsWith(bearerPrefix))
@ -429,6 +473,8 @@ public class QJavalinImplementation
setupSession(context, queryInput);
queryInput.setTableName(tableName);
queryInput.setShouldGenerateDisplayValues(true);
queryInput.setShouldTranslatePossibleValues(true);
// todo - validate that the primary key is of the proper type (e.g,. not a string for an id field)
// and throw a 400-series error (tell the user bad-request), rather than, we're doing a 500 (server error)
@ -524,6 +570,8 @@ public class QJavalinImplementation
QueryInput queryInput = new QueryInput(qInstance);
setupSession(context, queryInput);
queryInput.setTableName(context.pathParam("table"));
queryInput.setShouldGenerateDisplayValues(true);
queryInput.setShouldTranslatePossibleValues(true);
queryInput.setSkip(integerQueryParam(context, "skip"));
queryInput.setLimit(integerQueryParam(context, "limit"));
@ -836,4 +884,14 @@ public class QJavalinImplementation
return (null);
}
/*******************************************************************************
** Setter for qInstanceHotSwapSupplier
*******************************************************************************/
public static void setQInstanceHotSwapSupplier(Supplier<QInstance> qInstanceHotSwapSupplier)
{
QJavalinImplementation.qInstanceHotSwapSupplier = qInstanceHotSwapSupplier;
}
}

View File

@ -536,6 +536,11 @@ public class QPicoCliImplementation
queryInput.setSession(session);
queryInput.setTableName(tableName);
queryInput.setSkip(subParseResult.matchedOptionValue("skip", null));
// todo - think about these (e.g., based on user's requested output format?
// queryInput.setShouldGenerateDisplayValues(true);
// queryInput.setShouldTranslatePossibleValues(true);
String primaryKeyValue = subParseResult.matchedPositionalValue(0, null);
if(primaryKeyValue == null)
@ -581,6 +586,10 @@ public class QPicoCliImplementation
queryInput.setLimit(subParseResult.matchedOptionValue("limit", null));
queryInput.setFilter(generateQueryFilter(subParseResult));
// todo - think about these (e.g., based on user's requested output format?
// queryInput.setShouldGenerateDisplayValues(true);
// queryInput.setShouldTranslatePossibleValues(true);
QueryAction queryAction = new QueryAction();
QueryOutput queryOutput = queryAction.execute(queryInput);
commandLine.getOut().println(JsonUtils.toPrettyJson(queryOutput));

View File

@ -253,7 +253,7 @@ public class SampleMetaDataProvider
.withBackendName(RDBMS_BACKEND_NAME)
.withPrimaryKeyField("id")
.withRecordLabelFormat("%s %s")
.withRecordLabelFields(List.of("firstName", "lastName"))
.withRecordLabelFields("firstName", "lastName")
.withField(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false))
.withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME).withBackendName("create_date").withIsEditable(false))
.withField(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME).withBackendName("modify_date").withIsEditable(false))
@ -267,7 +267,7 @@ public class SampleMetaDataProvider
.withSection(new QFieldSection("identity", "Identity", new QIcon("badge"), Tier.T1, List.of("id", "firstName", "lastName")))
.withSection(new QFieldSection("basicInfo", "Basic Info", new QIcon("dataset"), Tier.T2, List.of("email", "birthDate")))
.withSection(new QFieldSection("employmentInfo", "Employment Info", new QIcon("work"), Tier.T2, List.of("annualSalary", "daysWorked")))
.withSection(new QFieldSection("employmentInfo", "Employment Info", new QIcon("work"), Tier.T2, List.of("isEmployed", "annualSalary", "daysWorked")))
.withSection(new QFieldSection("dates", "Dates", new QIcon("calendar_month"), Tier.T3, List.of("createDate", "modifyDate")));
QInstanceEnricher.setInferredFieldBackendNames(qTableMetaData);

View File

@ -31,15 +31,16 @@ CREATE TABLE person
birth_date DATE,
email VARCHAR(250) NOT NULL,
is_employed BOOLEAN,
annual_salary DECIMAL(12, 2),
days_worked INTEGER
);
INSERT INTO person (id, first_name, last_name, birth_date, email, annual_salary, days_worked) VALUES (1, 'Darin', 'Kelkhoff', '1980-05-31', 'darin.kelkhoff@gmail.com', 75003.50, 1001);
INSERT INTO person (id, first_name, last_name, birth_date, email, annual_salary, days_worked) VALUES (2, 'James', 'Maes', '1980-05-15', 'jmaes@mmltholdings.com', 150000, 10100);
INSERT INTO person (id, first_name, last_name, birth_date, email, annual_salary, days_worked) VALUES (3, 'Tim', 'Chamberlain', '1976-05-28', 'tchamberlain@mmltholdings.com', 300000, 100100);
INSERT INTO person (id, first_name, last_name, birth_date, email, annual_salary, days_worked) VALUES (4, 'Tyler', 'Samples', NULL, 'tsamples@mmltholdings.com', 950000, 75);
INSERT INTO person (id, first_name, last_name, birth_date, email, annual_salary, days_worked) VALUES (5, 'Garret', 'Richardson', '1981-01-01', 'grichardson@mmltholdings.com', 1500000, 1);
INSERT INTO person (id, first_name, last_name, birth_date, email, is_employed, annual_salary, days_worked) VALUES (1, 'Darin', 'Kelkhoff', '1980-05-31', 'darin.kelkhoff@gmail.com', 1, 75003.50, 1001);
INSERT INTO person (id, first_name, last_name, birth_date, email, is_employed, annual_salary, days_worked) VALUES (2, 'James', 'Maes', '1980-05-15', 'jmaes@mmltholdings.com', 1, 150000, 10100);
INSERT INTO person (id, first_name, last_name, birth_date, email, is_employed, annual_salary, days_worked) VALUES (3, 'Tim', 'Chamberlain', '1976-05-28', 'tchamberlain@mmltholdings.com', 1, 300000, 100100);
INSERT INTO person (id, first_name, last_name, birth_date, email, is_employed, annual_salary, days_worked) VALUES (4, 'Tyler', 'Samples', NULL, 'tsamples@mmltholdings.com', 1, 950000, 75);
INSERT INTO person (id, first_name, last_name, birth_date, email, is_employed, annual_salary, days_worked) VALUES (5, 'Garret', 'Richardson', '1981-01-01', 'grichardson@mmltholdings.com', 0, 1500000, 1);
DROP TABLE IF EXISTS carrier;
CREATE TABLE carrier