Merge pull request #6 from Kingsrook/feature/sprint-9-support-updates

Feature/sprint 9 support updates
This commit is contained in:
2022-08-25 09:03:10 -05:00
committed by GitHub
86 changed files with 4731 additions and 173 deletions

View File

@ -48,6 +48,7 @@
<maven.compiler.showWarnings>true</maven.compiler.showWarnings>
<coverage.haltOnFailure>true</coverage.haltOnFailure>
<coverage.instructionCoveredRatioMinimum>0.80</coverage.instructionCoveredRatioMinimum>
<coverage.classCoveredRatioMinimum>0.95</coverage.classCoveredRatioMinimum>
</properties>
<dependencyManagement>
@ -211,6 +212,11 @@
<value>COVEREDRATIO</value>
<minimum>${coverage.instructionCoveredRatioMinimum}</minimum>
</limit>
<limit>
<counter>CLASS</counter>
<value>COVEREDRATIO</value>
<minimum>${coverage.classCoveredRatioMinimum}</minimum>
</limit>
</limits>
</rule>
</rules>
@ -255,7 +261,7 @@ echo "------------------------------------------------------------"
which xpath > /dev/null 2>&1
if [ "$?" == "0" ]; then
echo "Element\nInstructions Missed\nInstruction Coverage\nBranches Missed\nBranch Coverage\nComplexity Missed\nComplexity Hit\nLines Missed\nLines Hit\nMethods Missed\nMethods Hit\nClasses Missed\nClasses Hit\n" > /tmp/$$.headers
xpath -q -e '/html/body/table/tfoot/tr[1]/td/text()' target/site/jacoco/index.html > /tmp/$$.values
xpath -n -q -e '/html/body/table/tfoot/tr[1]/td/text()' target/site/jacoco/index.html > /tmp/$$.values
paste /tmp/$$.headers /tmp/$$.values | tail +2 | awk -v FS='\t' '{printf("%-20s %s\n",$1,$2)}'
rm /tmp/$$.headers /tmp/$$.values
else

View File

@ -0,0 +1,124 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.customizers;
import java.util.Optional;
import java.util.function.Function;
import com.kingsrook.qqq.backend.core.actions.values.QCustomPossibleValueProvider;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/*******************************************************************************
** Utility to load code for running QQQ customizers.
*******************************************************************************/
public class QCodeLoader
{
private static final Logger LOG = LogManager.getLogger(QCodeLoader.class);
/*******************************************************************************
**
*******************************************************************************/
public static <T, R> Optional<Function<T, R>> getTableCustomizerFunction(QTableMetaData table, String customizerName)
{
Optional<QCodeReference> codeReference = table.getCustomizer(customizerName);
if(codeReference.isPresent())
{
return (Optional.ofNullable(QCodeLoader.getFunction(codeReference.get())));
}
return (Optional.empty());
}
/*******************************************************************************
**
*******************************************************************************/
@SuppressWarnings("unchecked")
public static <T, R> Function<T, R> getFunction(QCodeReference codeReference)
{
if(codeReference == null)
{
return (null);
}
if(!codeReference.getCodeType().equals(QCodeType.JAVA))
{
///////////////////////////////////////////////////////////////////////////////////////
// todo - 1) support more languages, 2) wrap them w/ java Functions here, 3) profit! //
///////////////////////////////////////////////////////////////////////////////////////
throw (new IllegalArgumentException("Only JAVA customizers are supported at this time."));
}
try
{
Class<?> customizerClass = Class.forName(codeReference.getName());
return ((Function<T, R>) customizerClass.getConstructor().newInstance());
}
catch(Exception e)
{
LOG.error("Error initializing customizer: " + codeReference);
//////////////////////////////////////////////////////////////////////////////////////////////////////////
// return null here - under the assumption that during normal run-time operations, we'll never hit here //
// as we'll want to validate all functions in the instance validator at startup time (and IT will throw //
// if it finds an invalid code reference //
//////////////////////////////////////////////////////////////////////////////////////////////////////////
return (null);
}
}
/*******************************************************************************
**
*******************************************************************************/
public static QCustomPossibleValueProvider getCustomPossibleValueProvider(QPossibleValueSource possibleValueSource) throws QException
{
try
{
Class<?> codeClass = Class.forName(possibleValueSource.getCustomCodeReference().getName());
Object codeObject = codeClass.getConstructor().newInstance();
if(!(codeObject instanceof QCustomPossibleValueProvider customPossibleValueProvider))
{
throw (new QException("The supplied code [" + codeClass.getName() + "] is not an instance of QCustomPossibleValueProvider"));
}
return (customPossibleValueProvider);
}
catch(QException qe)
{
throw (qe);
}
catch(Exception e)
{
throw (new QException("Error getting custom possible value provider for PVS [" + possibleValueSource.getName() + "]", e));
}
}
}

View File

@ -0,0 +1,61 @@
package com.kingsrook.qqq.backend.core.actions.customizers;
import java.util.function.Consumer;
/*******************************************************************************
** Object used by TableCustomizers enum (and similar enums in backend modules)
** to assist with definition and validation of Customizers applied to tables.
*******************************************************************************/
public class TableCustomizer
{
private final String role;
private final Class<?> expectedType;
private final Consumer<Object> validationFunction;
/*******************************************************************************
**
*******************************************************************************/
public TableCustomizer(String role, Class<?> expectedType, Consumer<Object> validationFunction)
{
this.role = role;
this.expectedType = expectedType;
this.validationFunction = validationFunction;
}
/*******************************************************************************
** Getter for role
**
*******************************************************************************/
public String getRole()
{
return role;
}
/*******************************************************************************
** Getter for expectedType
**
*******************************************************************************/
public Class<?> getExpectedType()
{
return expectedType;
}
/*******************************************************************************
** Getter for validationFunction
**
*******************************************************************************/
public Consumer<Object> getValidationFunction()
{
return validationFunction;
}
}

View File

@ -0,0 +1,85 @@
package com.kingsrook.qqq.backend.core.actions.customizers;
import java.util.function.Function;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
/*******************************************************************************
** Enum definition of possible table customizers - "roles" for custom code that
** can be applied to tables.
**
** Works with TableCustomizer (singular version of this name) objects, during
** instance validation, to provide validation of the referenced code (and to
** make such validation from sub-backend-modules possible in the future).
**
** The idea of the 3rd argument here is to provide a way that we can enforce
** the type-parameters for the custom code. E.g., if it's a Function - how
** can we check at run-time that the type-params are correct? We couldn't find
** how to do this "reflectively", so we can instead try to run the custom code,
** passing it objects of the type that this customizer expects, and a validation
** error will raise upon ClassCastException... This maybe could improve!
*******************************************************************************/
public enum TableCustomizers
{
POST_QUERY_RECORD(new TableCustomizer("postQueryRecord", Function.class, ((Object x) ->
{
Function<QRecord, QRecord> function = (Function<QRecord, QRecord>) x;
QRecord output = function.apply(new QRecord());
})));
private final TableCustomizer tableCustomizer;
/*******************************************************************************
**
*******************************************************************************/
TableCustomizers(TableCustomizer tableCustomizer)
{
this.tableCustomizer = tableCustomizer;
}
/*******************************************************************************
** Get the TableCustomer for a given role (e.g., the role used in meta-data, not
** the enum-constant name).
*******************************************************************************/
public static TableCustomizers forRole(String name)
{
for(TableCustomizers value : values())
{
if(value.tableCustomizer.getRole().equals(name))
{
return (value);
}
}
return (null);
}
/*******************************************************************************
** Getter for tableCustomizer
**
*******************************************************************************/
public TableCustomizer getTableCustomizer()
{
return tableCustomizer;
}
/*******************************************************************************
** get the role from the tableCustomizer
**
*******************************************************************************/
public String getRole()
{
return (tableCustomizer.getRole());
}
}

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

@ -26,6 +26,7 @@ import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.utils.SleepUtils;
import org.apache.logging.log4j.LogManager;
@ -42,12 +43,42 @@ public class RecordPipe
private ArrayBlockingQueue<QRecord> queue = new ArrayBlockingQueue<>(1_000);
private Consumer<List<QRecord>> postRecordActions = null;
/////////////////////////////////////
// See usage below for explanation //
/////////////////////////////////////
private List<QRecord> singleRecordListForPostRecordActions = new ArrayList<>();
/*******************************************************************************
** Add a record to the pipe
** Returns true iff the record fit in the pipe; false if the pipe is currently full.
** Add a record to the pipe. Will block if the pipe is full.
*******************************************************************************/
public void addRecord(QRecord record)
{
if(postRecordActions != null)
{
////////////////////////////////////////////////////////////////////////////////////
// the initial use-case of this method is to call QueryAction.postRecordActions //
// that method requires that the list param be modifiable. Originally we used //
// List.of here - but that is immutable, so, instead use this single-record-list //
// (which we'll create as a field in this class, to avoid always re-constructing) //
////////////////////////////////////////////////////////////////////////////////////
singleRecordListForPostRecordActions.add(record);
postRecordActions.accept(singleRecordListForPostRecordActions);
record = singleRecordListForPostRecordActions.remove(0);
}
doAddRecord(record);
}
/*******************************************************************************
** Private internal version of add record - assumes the postRecordActions have
** already ran.
*******************************************************************************/
private void doAddRecord(QRecord record)
{
boolean offerResult = queue.offer(record);
@ -66,7 +97,15 @@ public class RecordPipe
*******************************************************************************/
public void addRecords(List<QRecord> records)
{
records.forEach(this::addRecord);
if(postRecordActions != null)
{
postRecordActions.accept(records);
}
//////////////////////////////////////////////////////////////////////////////////////////////////
// make sure to go to the private version of doAddRecord - to avoid re-running the post-actions //
//////////////////////////////////////////////////////////////////////////////////////////////////
records.forEach(this::doAddRecord);
}
@ -101,4 +140,14 @@ public class RecordPipe
return (queue.size());
}
/*******************************************************************************
**
*******************************************************************************/
public void setPostRecordActions(Consumer<List<QRecord>> postRecordActions)
{
this.postRecordActions = postRecordActions;
}
}

View File

@ -22,11 +22,18 @@
package com.kingsrook.qqq.backend.core.actions.tables;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
import com.kingsrook.qqq.backend.core.actions.values.QPossibleValueTranslator;
import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
@ -37,6 +44,14 @@ import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
*******************************************************************************/
public class QueryAction
{
private Optional<Function<QRecord, QRecord>> postQueryRecordCustomizer;
private QueryInput queryInput;
private QValueFormatter qValueFormatter;
private QPossibleValueTranslator qPossibleValueTranslator;
/*******************************************************************************
**
*******************************************************************************/
@ -44,18 +59,58 @@ public class QueryAction
{
ActionHelper.validateSession(queryInput);
postQueryRecordCustomizer = QCodeLoader.getTableCustomizerFunction(queryInput.getTable(), TableCustomizers.POST_QUERY_RECORD.getRole());
this.queryInput = queryInput;
if(queryInput.getRecordPipe() != null)
{
queryInput.getRecordPipe().setPostRecordActions(this::postRecordActions);
}
QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher();
QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(queryInput.getBackend());
QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(queryInput.getBackend());
// todo pre-customization - just get to modify the request?
QueryOutput queryOutput = qModule.getQueryInterface().execute(queryInput);
// todo post-customization - can do whatever w/ the result if you want
if (queryInput.getRecordPipe() == null)
if(queryInput.getRecordPipe() == null)
{
QValueFormatter.setDisplayValuesInRecords(queryInput.getTable(), queryOutput.getRecords());
postRecordActions(queryOutput.getRecords());
}
return queryOutput;
}
/*******************************************************************************
** Run the necessary actions on a list of records (which must be a mutable list - e.g.,
** not one created via List.of()). This may include setting display values,
** translating possible values, and running post-record customizations.
*******************************************************************************/
public void postRecordActions(List<QRecord> records)
{
if(this.postQueryRecordCustomizer.isPresent())
{
records.replaceAll(t -> postQueryRecordCustomizer.get().apply(t));
}
if(queryInput.getShouldGenerateDisplayValues())
{
if(qValueFormatter == null)
{
qValueFormatter = new QValueFormatter();
}
qValueFormatter.setDisplayValuesInRecords(queryInput.getTable(), records);
}
if(queryInput.getShouldTranslatePossibleValues())
{
if(qPossibleValueTranslator == null)
{
qPossibleValueTranslator = new QPossibleValueTranslator(queryInput.getInstance(), queryInput.getSession());
}
qPossibleValueTranslator.translatePossibleValuesInRecords(queryInput.getTable(), records);
}
}
}

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,355 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <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.customizers.QCodeLoader;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValue;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.ListingHash;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/*******************************************************************************
** Class responsible for looking up possible-values for fields/records and
** make them into display values.
*******************************************************************************/
public class QPossibleValueTranslator
{
private static final Logger LOG = LogManager.getLogger(QPossibleValueTranslator.class);
private final QInstance qInstance;
private final QSession session;
///////////////////////////////////////////////////////
// top-level keys are pvsNames (not table names) //
// 2nd-level keys are pkey values from the PVS table //
///////////////////////////////////////////////////////
private Map<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);
}
String resultValue = null;
if(possibleValueSource.getType().equals(QPossibleValueSourceType.ENUM))
{
resultValue = translatePossibleValueEnum(value, possibleValueSource);
}
else if(possibleValueSource.getType().equals(QPossibleValueSourceType.TABLE))
{
resultValue = translatePossibleValueTable(field, value, possibleValueSource);
}
else if(possibleValueSource.getType().equals(QPossibleValueSourceType.CUSTOM))
{
resultValue = translatePossibleValueCustom(field, value, possibleValueSource);
}
else
{
LOG.error("Unrecognized possibleValueSourceType [" + possibleValueSource.getType() + "] in PVS named [" + possibleValueSource.getName() + "] on field [" + field.getName() + "]");
}
if(resultValue == null)
{
resultValue = getDefaultForPossibleValue(possibleValueSource, value);
}
return (resultValue);
}
/*******************************************************************************
**
*******************************************************************************/
private String translatePossibleValueEnum(Serializable value, QPossibleValueSource possibleValueSource)
{
for(QPossibleValue<?> possibleValue : possibleValueSource.getEnumValues())
{
if(possibleValue.getId().equals(value))
{
return (formatPossibleValue(possibleValueSource, possibleValue));
}
}
return (null);
}
/*******************************************************************************
**
*******************************************************************************/
private String translatePossibleValueTable(QFieldMetaData field, Serializable value, QPossibleValueSource possibleValueSource)
{
/////////////////////////////////
// null input gets null output //
/////////////////////////////////
if(value == null)
{
return (null);
}
//////////////////////////////////////////////////////////////
// look for cached value - if it's missing, call the primer //
//////////////////////////////////////////////////////////////
possibleValueCache.putIfAbsent(possibleValueSource.getName(), new HashMap<>());
Map<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 translatePossibleValueCustom(QFieldMetaData field, Serializable value, QPossibleValueSource possibleValueSource)
{
try
{
QCustomPossibleValueProvider customPossibleValueProvider = QCodeLoader.getCustomPossibleValueProvider(possibleValueSource);
return (formatPossibleValue(possibleValueSource, customPossibleValueProvider.getPossibleValue(value)));
}
catch(Exception e)
{
LOG.warn("Error sending [" + value + "] for field [" + field + "] through custom code for PVS [" + field.getPossibleValueSourceName() + "]", e);
}
return (null);
}
/*******************************************************************************
**
*******************************************************************************/
private String formatPossibleValue(QPossibleValueSource possibleValueSource, QPossibleValue<?> possibleValue)
{
return (doFormatPossibleValue(possibleValueSource.getValueFormat(), possibleValueSource.getValueFields(), possibleValue.getId(), possibleValue.getLabel()));
}
/*******************************************************************************
**
*******************************************************************************/
private String getDefaultForPossibleValue(QPossibleValueSource possibleValueSource, Serializable value)
{
if(possibleValueSource.getValueFormatIfNotFound() == null)
{
return (null);
}
return (doFormatPossibleValue(possibleValueSource.getValueFormatIfNotFound(), possibleValueSource.getValueFieldsIfNotFound(), value, null));
}
/*******************************************************************************
**
*******************************************************************************/
@SuppressWarnings("checkstyle:Indentation")
private String doFormatPossibleValue(String formatString, List<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()));
}
/*******************************************************************************
** prime the cache (e.g., by doing bulk-queries) for table-based PVS's
**
** @param table the table that the records are from
** @param records the records that have the possible value id's (e.g., foreign keys)
*******************************************************************************/
void primePvsCache(QTableMetaData table, List<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

@ -34,7 +34,8 @@ import org.apache.logging.log4j.Logger;
/*******************************************************************************
** Utility to apply display formats to values for fields
** Utility to apply display formats to values for records and fields.
**
*******************************************************************************/
public class QValueFormatter
{
@ -45,7 +46,7 @@ public class QValueFormatter
/*******************************************************************************
**
*******************************************************************************/
public static String formatValue(QFieldMetaData field, Serializable value)
public String formatValue(QFieldMetaData field, Serializable value)
{
//////////////////////////////////
// null values get null results //
@ -68,6 +69,7 @@ public class QValueFormatter
{
try
{
// todo - revisit if we actually want this - or - if you should get an error if you mis-configure your table this way (ideally during validation!)
if(e.getMessage().equals("f != java.lang.Integer"))
{
return formatValue(field, ValueUtils.getValueAsBigDecimal(value));
@ -99,7 +101,7 @@ public class QValueFormatter
/*******************************************************************************
** Make a string from a table's recordLabelFormat and fields, for a given record.
*******************************************************************************/
public static String formatRecordLabel(QTableMetaData table, QRecord record)
public String formatRecordLabel(QTableMetaData table, QRecord record)
{
if(!StringUtils.hasContent(table.getRecordLabelFormat()))
{
@ -128,7 +130,7 @@ public class QValueFormatter
/*******************************************************************************
** Deal with non-happy-path cases for making a record label.
*******************************************************************************/
private static String formatRecordLabelExceptionalCases(QTableMetaData table, QRecord record)
private String formatRecordLabelExceptionalCases(QTableMetaData table, QRecord record)
{
///////////////////////////////////////////////////////////////////////////////////////
// if there's no record label format, then just return the primary key display value //
@ -156,7 +158,7 @@ public class QValueFormatter
/*******************************************************************************
** For a list of records, set their recordLabels and display values
*******************************************************************************/
public static void setDisplayValuesInRecords(QTableMetaData table, List<QRecord> records)
public void setDisplayValuesInRecords(QTableMetaData table, List<QRecord> records)
{
if(records == null)
{
@ -167,11 +169,11 @@ public class QValueFormatter
{
for(QFieldMetaData field : table.getFields().values())
{
String formattedValue = QValueFormatter.formatValue(field, record.getValue(field.getName()));
String formattedValue = formatValue(field, record.getValue(field.getName()));
record.setDisplayValue(field.getName(), formattedValue);
}
record.setRecordLabel(QValueFormatter.formatRecordLabel(table, record));
record.setRecordLabel(formatRecordLabel(table, record));
}
}

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

@ -57,7 +57,7 @@ public class QInstanceValidationException extends QException
{
super(
(reasons != null && reasons.size() > 0)
? "Instance validation failed for the following reasons: " + StringUtils.joinWithCommasAndAnd(reasons)
? "Instance validation failed for the following reasons:\n - " + StringUtils.join("\n - ", reasons)
: "Validation failed, but no reasons were provided");
if(reasons != null && reasons.size() > 0)

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,21 @@ public class QInstanceEnricher
return (name.substring(0, 1).toUpperCase(Locale.ROOT));
}
return (name.substring(0, 1).toUpperCase(Locale.ROOT) + name.substring(1).replaceAll("([A-Z])", " $1"));
String suffix = name.substring(1)
//////////////////////////////////////////////////////////////////////
// Put a space before capital letters or numbers embedded in a name //
// e.g., omethingElse -> omething Else; umber1 -> umber 1 //
//////////////////////////////////////////////////////////////////////
.replaceAll("([A-Z0-9]+)", " $1")
////////////////////////////////////////////////////////////////
// put a space between numbers and words that come after them //
// e.g., umber1dad -> number 1 dad //
////////////////////////////////////////////////////////////////
.replaceAll("([0-9])([A-Za-z])", "$1 $2");
return (name.substring(0, 1).toUpperCase(Locale.ROOT) + suffix);
}
@ -579,7 +599,7 @@ public class QInstanceEnricher
{
for(String fieldName : table.getRecordLabelFields())
{
if(!usedFieldNames.contains(fieldName))
if(!usedFieldNames.contains(fieldName) && table.getFields().containsKey(fieldName))
{
identitySection.getFieldNames().add(fieldName);
usedFieldNames.add(fieldName);

View File

@ -22,13 +22,21 @@
package com.kingsrook.qqq.backend.core.instances;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Consumer;
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
import com.kingsrook.qqq.backend.core.actions.values.QCustomPossibleValueProvider;
import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeUsage;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppChildMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData;
@ -37,6 +45,8 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/*******************************************************************************
@ -51,6 +61,11 @@ import com.kingsrook.qqq.backend.core.utils.StringUtils;
*******************************************************************************/
public class QInstanceValidator
{
private static final Logger LOG = LogManager.getLogger(QInstanceValidator.class);
private boolean printWarnings = false;
/*******************************************************************************
**
@ -88,6 +103,7 @@ public class QInstanceValidator
validateTables(qInstance, errors);
validateProcesses(qInstance, errors);
validateApps(qInstance, errors);
validatePossibleValueSources(qInstance, errors);
}
catch(Exception e)
{
@ -167,8 +183,8 @@ public class QInstanceValidator
//////////////////////////////////////////
// validate field sections in the table //
//////////////////////////////////////////
Set<String> fieldNamesInSections = new HashSet<>();
QFieldSection tier1Section = null;
Set<String> fieldNamesInSections = new HashSet<>();
QFieldSection tier1Section = null;
if(table.getSections() != null)
{
for(QFieldSection section : table.getSections())
@ -190,12 +206,140 @@ public class QInstanceValidator
}
}
///////////////////////////////
// validate the record label //
///////////////////////////////
if(table.getRecordLabelFields() != null)
{
for(String recordLabelField : table.getRecordLabelFields())
{
assertCondition(errors, table.getFields().containsKey(recordLabelField), "Table " + tableName + " record label field " + recordLabelField + " is not a field on this table.");
}
}
if(table.getCustomizers() != null)
{
for(Map.Entry<String, QCodeReference> entry : table.getCustomizers().entrySet())
{
validateTableCustomizer(errors, tableName, entry.getKey(), entry.getValue());
}
}
});
}
}
/*******************************************************************************
**
*******************************************************************************/
private void validateTableCustomizer(List<String> errors, String tableName, String customizerName, QCodeReference codeReference)
{
String prefix = "Table " + tableName + ", customizer " + customizerName + ": ";
if(!preAssertionsForCodeReference(errors, codeReference, prefix))
{
return;
}
//////////////////////////////////////////////////////////////////////////////
// make sure (at this time) that it's a java type, then do some java checks //
//////////////////////////////////////////////////////////////////////////////
if(assertCondition(errors, codeReference.getCodeType().equals(QCodeType.JAVA), prefix + "Only JAVA customizers are supported at this time."))
{
///////////////////////////////////////
// make sure the class can be loaded //
///////////////////////////////////////
Class<?> customizerClass = getClassForCodeReference(errors, codeReference, prefix);
if(customizerClass != null)
{
//////////////////////////////////////////////////
// make sure the customizer can be instantiated //
//////////////////////////////////////////////////
Object customizerInstance = getInstanceOfCodeReference(errors, prefix, customizerClass);
TableCustomizers tableCustomizer = TableCustomizers.forRole(customizerName);
if(tableCustomizer == null)
{
////////////////////////////////////////////////////////////////////////////////////////////////////
// todo - in the future, load customizers from backend-modules (e.g., FilesystemTableCustomizers) //
////////////////////////////////////////////////////////////////////////////////////////////////////
warn(prefix + "Unrecognized table customizer name (at least at backend-core level)");
}
else
{
////////////////////////////////////////////////////////////////////////
// make sure the customizer instance can be cast to the expected type //
////////////////////////////////////////////////////////////////////////
if(customizerInstance != null && tableCustomizer.getTableCustomizer().getExpectedType() != null)
{
Object castedObject = getCastedObject(errors, prefix, tableCustomizer.getTableCustomizer().getExpectedType(), customizerInstance);
Consumer<Object> validationFunction = tableCustomizer.getTableCustomizer().getValidationFunction();
if(castedObject != null && validationFunction != null)
{
try
{
validationFunction.accept(castedObject);
}
catch(ClassCastException e)
{
errors.add(prefix + "Error validating customizer type parameters: " + e.getMessage());
}
catch(Exception e)
{
///////////////////////////////////////////////////////////////////////////////////////////////////////////
// mmm, calling customizers w/ random data is expected to often throw, so, this check is iffy at best... //
// if we run into more trouble here, we might consider disabling the whole "validation function" check. //
///////////////////////////////////////////////////////////////////////////////////////////////////////////
}
}
}
}
}
}
}
/*******************************************************************************
**
*******************************************************************************/
private <T> T getCastedObject(List<String> errors, String prefix, Class<T> expectedType, Object customizerInstance)
{
T castedObject = null;
try
{
castedObject = expectedType.cast(customizerInstance);
}
catch(ClassCastException e)
{
errors.add(prefix + "CodeReference could not be casted to the expected type: " + expectedType);
}
return castedObject;
}
/*******************************************************************************
**
*******************************************************************************/
private Object getInstanceOfCodeReference(List<String> errors, String prefix, Class<?> customizerClass)
{
Object customizerInstance = null;
try
{
customizerInstance = customizerClass.getConstructor().newInstance();
}
catch(InvocationTargetException | InstantiationException | IllegalAccessException | NoSuchMethodException e)
{
errors.add(prefix + "Instance of CodeReference could not be created: " + e);
}
return customizerInstance;
}
/*******************************************************************************
**
*******************************************************************************/
@ -225,7 +369,7 @@ public class QInstanceValidator
*******************************************************************************/
private void validateProcesses(QInstance qInstance, List<String> errors)
{
if(!CollectionUtils.nullSafeIsEmpty(qInstance.getProcesses()))
if(CollectionUtils.nullSafeHasContents(qInstance.getProcesses()))
{
qInstance.getProcesses().forEach((processName, process) ->
{
@ -264,7 +408,7 @@ public class QInstanceValidator
*******************************************************************************/
private void validateApps(QInstance qInstance, List<String> errors)
{
if(!CollectionUtils.nullSafeIsEmpty(qInstance.getApps()))
if(CollectionUtils.nullSafeHasContents(qInstance.getApps()))
{
qInstance.getApps().forEach((appName, app) ->
{
@ -291,6 +435,142 @@ public class QInstanceValidator
/*******************************************************************************
**
*******************************************************************************/
private void validatePossibleValueSources(QInstance qInstance, List<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() + ".");
if(assertCondition(errors, possibleValueSource.getType() != null, "Missing type for possibleValueSource: " + pvsName))
{
////////////////////////////////////////////////////////////////////////////////////////////////
// assert about fields that should and should not be set, based on possible value source type //
// do additional type-specific validations as well //
////////////////////////////////////////////////////////////////////////////////////////////////
switch(possibleValueSource.getType())
{
case ENUM ->
{
assertCondition(errors, !StringUtils.hasContent(possibleValueSource.getTableName()), "enum-type possibleValueSource " + pvsName + " should not have a tableName.");
assertCondition(errors, possibleValueSource.getCustomCodeReference() == null, "enum-type possibleValueSource " + pvsName + " should not have a customCodeReference.");
assertCondition(errors, CollectionUtils.nullSafeHasContents(possibleValueSource.getEnumValues()), "enum-type possibleValueSource " + pvsName + " is missing enum values");
}
case TABLE ->
{
assertCondition(errors, CollectionUtils.nullSafeIsEmpty(possibleValueSource.getEnumValues()), "table-type possibleValueSource " + pvsName + " should not have enum values.");
assertCondition(errors, possibleValueSource.getCustomCodeReference() == null, "table-type possibleValueSource " + pvsName + " should not have a customCodeReference.");
if(assertCondition(errors, StringUtils.hasContent(possibleValueSource.getTableName()), "table-type possibleValueSource " + pvsName + " is missing a tableName."))
{
assertCondition(errors, qInstance.getTable(possibleValueSource.getTableName()) != null, "Unrecognized table " + possibleValueSource.getTableName() + " for possibleValueSource " + pvsName + ".");
}
}
case CUSTOM ->
{
assertCondition(errors, CollectionUtils.nullSafeIsEmpty(possibleValueSource.getEnumValues()), "custom-type possibleValueSource " + pvsName + " should not have enum values.");
assertCondition(errors, !StringUtils.hasContent(possibleValueSource.getTableName()), "custom-type possibleValueSource " + pvsName + " should not have a tableName.");
if(assertCondition(errors, possibleValueSource.getCustomCodeReference() != null, "custom-type possibleValueSource " + pvsName + " is missing a customCodeReference."))
{
assertCondition(errors, QCodeUsage.POSSIBLE_VALUE_PROVIDER.equals(possibleValueSource.getCustomCodeReference().getCodeUsage()), "customCodeReference for possibleValueSource " + pvsName + " is not a possibleValueProvider.");
validateCustomPossibleValueSourceCode(errors, pvsName, possibleValueSource.getCustomCodeReference());
}
}
default -> errors.add("Unexpected possibleValueSource type: " + possibleValueSource.getType());
}
}
});
}
}
/*******************************************************************************
**
*******************************************************************************/
private void validateCustomPossibleValueSourceCode(List<String> errors, String pvsName, QCodeReference codeReference)
{
String prefix = "PossibleValueSource " + pvsName + " custom code reference: ";
if(!preAssertionsForCodeReference(errors, codeReference, prefix))
{
return;
}
//////////////////////////////////////////////////////////////////////////////
// make sure (at this time) that it's a java type, then do some java checks //
//////////////////////////////////////////////////////////////////////////////
if(assertCondition(errors, codeReference.getCodeType().equals(QCodeType.JAVA), prefix + "Only JAVA customizers are supported at this time."))
{
///////////////////////////////////////
// make sure the class can be loaded //
///////////////////////////////////////
Class<?> customizerClass = getClassForCodeReference(errors, codeReference, prefix);
if(customizerClass != null)
{
//////////////////////////////////////////////////
// make sure the customizer can be instantiated //
//////////////////////////////////////////////////
Object customizerInstance = getInstanceOfCodeReference(errors, prefix, customizerClass);
////////////////////////////////////////////////////////////////////////
// make sure the customizer instance can be cast to the expected type //
////////////////////////////////////////////////////////////////////////
if(customizerInstance != null)
{
getCastedObject(errors, prefix, QCustomPossibleValueProvider.class, customizerInstance);
}
}
}
}
/*******************************************************************************
**
*******************************************************************************/
private Class<?> getClassForCodeReference(List<String> errors, QCodeReference codeReference, String prefix)
{
Class<?> customizerClass = null;
try
{
customizerClass = Class.forName(codeReference.getName());
}
catch(ClassNotFoundException e)
{
errors.add(prefix + "Class for CodeReference could not be found.");
}
return customizerClass;
}
/*******************************************************************************
**
*******************************************************************************/
private boolean preAssertionsForCodeReference(List<String> errors, QCodeReference codeReference, String prefix)
{
boolean okay = true;
if(!assertCondition(errors, StringUtils.hasContent(codeReference.getName()), prefix + " is missing a code reference name"))
{
okay = false;
}
if(!assertCondition(errors, codeReference.getCodeType() != null, prefix + " is missing a code type"))
{
okay = false;
}
return (okay);
}
/*******************************************************************************
** Check if an app's child list can recursively be traversed without finding a
** duplicate, which would indicate a cycle (e.g., an error)
@ -343,4 +623,16 @@ public class QInstanceValidator
return (condition);
}
/*******************************************************************************
**
*******************************************************************************/
private void warn(String message)
{
if(printWarnings)
{
LOG.info("Validation warning: " + message);
}
}
}

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

@ -25,6 +25,7 @@ package com.kingsrook.qqq.backend.core.model.data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalTime;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
@ -328,6 +329,16 @@ public class QRecord implements Serializable
/*******************************************************************************
**
*******************************************************************************/
public LocalTime getValueLocalTime(String fieldName)
{
return (ValueUtils.getValueAsLocalTime(values.get(fieldName)));
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -54,10 +54,10 @@ public class QInstance
////////////////////////////////////////////////////////////////////////////////////////////
// Important to use LinkedHashmap here, to preserve the order in which entries are added. //
////////////////////////////////////////////////////////////////////////////////////////////
private Map<String, QTableMetaData> tables = new LinkedHashMap<>();
private Map<String, QPossibleValueSource<?>> possibleValueSources = new LinkedHashMap<>();
private Map<String, QProcessMetaData> processes = new LinkedHashMap<>();
private Map<String, QAppMetaData> apps = new LinkedHashMap<>();
private Map<String, QTableMetaData> tables = new LinkedHashMap<>();
private Map<String, QPossibleValueSource> possibleValueSources = new LinkedHashMap<>();
private Map<String, QProcessMetaData> processes = new LinkedHashMap<>();
private Map<String, QAppMetaData> apps = new LinkedHashMap<>();
// todo - lock down the object (no more changes allowed) after it's been validated?
@ -190,7 +190,7 @@ public class QInstance
/*******************************************************************************
**
*******************************************************************************/
public void addPossibleValueSource(QPossibleValueSource<?> possibleValueSource)
public void addPossibleValueSource(QPossibleValueSource possibleValueSource)
{
this.addPossibleValueSource(possibleValueSource.getName(), possibleValueSource);
}
@ -353,7 +353,7 @@ public class QInstance
** Getter for possibleValueSources
**
*******************************************************************************/
public Map<String, QPossibleValueSource<?>> getPossibleValueSources()
public Map<String, QPossibleValueSource> getPossibleValueSources()
{
return possibleValueSources;
}
@ -364,7 +364,7 @@ public class QInstance
** Setter for possibleValueSources
**
*******************************************************************************/
public void setPossibleValueSources(Map<String, QPossibleValueSource<?>> possibleValueSources)
public void setPossibleValueSources(Map<String, QPossibleValueSource> possibleValueSources)
{
this.possibleValueSources = possibleValueSources;
}

View File

@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.model.metadata.code;
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
import com.kingsrook.qqq.backend.core.actions.values.QCustomPossibleValueProvider;
/*******************************************************************************
@ -70,6 +71,10 @@ public class QCodeReference
{
this.codeUsage = QCodeUsage.BACKEND_STEP;
}
else if(QCustomPossibleValueProvider.class.isAssignableFrom(javaClass))
{
this.codeUsage = QCodeUsage.POSSIBLE_VALUE_PROVIDER;
}
else
{
throw (new IllegalStateException("Unable to infer code usage type for class: " + javaClass.getName()));

View File

@ -29,5 +29,6 @@ package com.kingsrook.qqq.backend.core.model.metadata.code;
public enum QCodeUsage
{
BACKEND_STEP, // a backend-step in a process
CUSTOMIZER // a function to customize part of a QQQ table's behavior
CUSTOMIZER, // a function to customize part of a QQQ table's behavior
POSSIBLE_VALUE_PROVIDER // code that drives a custom possibleValueSource
}

View File

@ -133,6 +133,11 @@ public class QFieldMetaData
{
setDisplayFormat(fieldAnnotation.displayFormat());
}
if(StringUtils.hasContent(fieldAnnotation.possibleValueSourceName()))
{
setPossibleValueSourceName(fieldAnnotation.possibleValueSourceName());
}
}
}
catch(QException qe)
@ -406,6 +411,7 @@ public class QFieldMetaData
}
/*******************************************************************************
** Getter for displayFormat
**
@ -427,6 +433,7 @@ public class QFieldMetaData
}
/*******************************************************************************
** Fluent setter for displayFormat
**

View File

@ -36,11 +36,13 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
@JsonInclude(Include.NON_NULL)
public class QFrontendFieldMetaData
{
private String name;
private String label;
private String name;
private String label;
private QFieldType type;
private boolean isRequired;
private boolean isEditable;
private boolean isRequired;
private boolean isEditable;
private String possibleValueSourceName;
private String displayFormat;
//////////////////////////////////////////////////////////////////////////////////
// do not add setters. take values from the source-object in the constructor!! //
@ -58,6 +60,8 @@ public class QFrontendFieldMetaData
this.type = fieldMetaData.getType();
this.isRequired = fieldMetaData.getIsRequired();
this.isEditable = fieldMetaData.getIsEditable();
this.possibleValueSourceName = fieldMetaData.getPossibleValueSourceName();
this.displayFormat = fieldMetaData.getDisplayFormat();
}
@ -115,4 +119,26 @@ public class QFrontendFieldMetaData
return isEditable;
}
/*******************************************************************************
** Getter for displayFormat
**
*******************************************************************************/
public String getDisplayFormat()
{
return displayFormat;
}
/*******************************************************************************
** Getter for possibleValueSourceName
**
*******************************************************************************/
public String getPossibleValueSourceName()
{
return possibleValueSourceName;
}
}

View File

@ -0,0 +1,55 @@
package com.kingsrook.qqq.backend.core.model.metadata.possiblevalues;
import java.util.List;
/*******************************************************************************
** Define some standard ways to format the value portion of a PossibleValueSource.
**
** Can be passed to short-cut {set,with}ValueFormatAndFields methods in QPossibleValueSource
** class, or the format & field properties can be extracted and passed to regular field-level setters.
*******************************************************************************/
public enum PVSValueFormatAndFields
{
LABEL_ONLY("%s", "label"),
LABEL_PARENS_ID("%s (%s)", "label", "id"),
ID_COLON_LABEL("%s: %s", "id", "label");
private final String format;
private final List<String> fields;
/*******************************************************************************
**
*******************************************************************************/
PVSValueFormatAndFields(String format, String... fields)
{
this.format = format;
this.fields = List.of(fields);
}
/*******************************************************************************
** Getter for format
**
*******************************************************************************/
public String getFormat()
{
return format;
}
/*******************************************************************************
** Getter for fields
**
*******************************************************************************/
public List<String> getFields()
{
return fields;
}
}

View File

@ -0,0 +1,40 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.metadata.possiblevalues;
/*******************************************************************************
** Interface to be implemented by enums which can be used as a PossibleValueSource.
**
*******************************************************************************/
public interface PossibleValueEnum<T>
{
/*******************************************************************************
**
*******************************************************************************/
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,42 @@ package com.kingsrook.qqq.backend.core.model.metadata.possiblevalues;
import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
/*******************************************************************************
** Meta-data to represent a single field in a table.
** Meta-data to represent a "Possible value" - e.g., a translation of a foreign
** key and/or a limited set of "possible values" for a field (e.g., from a foreign
** table or an enum).
**
*******************************************************************************/
public class QPossibleValueSource<T>
public class QPossibleValueSource
{
private String name;
private String name;
private QPossibleValueSourceType type;
// should these be in sub-types??
private List<T> enumValues;
private String valueFormat = PVSValueFormatAndFields.LABEL_ONLY.getFormat();
private List<String> valueFields = PVSValueFormatAndFields.LABEL_ONLY.getFields();
private String valueFormatIfNotFound = null;
private List<String> valueFieldsIfNotFound = null;
// todo - optimization hints, such as "table is static, fully cache" or "table is small, so we can pull the whole thing into memory?"
//////////////////////
// for type = TABLE //
//////////////////////
private String tableName;
// todo - override labelFormat & labelFields?
/////////////////////
// for type = ENUM //
/////////////////////
private List<QPossibleValue<?>> enumValues;
///////////////////////
// for type = CUSTOM //
///////////////////////
private QCodeReference customCodeReference;
@ -72,7 +95,7 @@ public class QPossibleValueSource<T>
/*******************************************************************************
**
*******************************************************************************/
public QPossibleValueSource<T> withName(String name)
public QPossibleValueSource withName(String name)
{
this.name = name;
return (this);
@ -103,7 +126,7 @@ public class QPossibleValueSource<T>
/*******************************************************************************
**
*******************************************************************************/
public QPossibleValueSource<T> withType(QPossibleValueSourceType type)
public QPossibleValueSource withType(QPossibleValueSourceType type)
{
this.type = type;
return (this);
@ -111,11 +134,181 @@ public class QPossibleValueSource<T>
/*******************************************************************************
** 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 +319,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 +330,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 +339,89 @@ 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);
}
/*******************************************************************************
** This is the easiest way to add the values from an enum to a PossibleValueSource.
** Make sure the enum implements PossibleValueEnum - then call as:
** myPossibleValueSource.withValuesFromEnum(MyEnum.values()));
**
*******************************************************************************/
public <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);
}
/*******************************************************************************
**
*******************************************************************************/
public void setValueFormatAndFields(PVSValueFormatAndFields valueFormatAndFields)
{
this.valueFormat = valueFormatAndFields.getFormat();
this.valueFields = valueFormatAndFields.getFields();
}
/*******************************************************************************
**
*******************************************************************************/
public QPossibleValueSource withValueFormatAndFields(PVSValueFormatAndFields valueFormatAndFields)
{
setValueFormatAndFields(valueFormatAndFields);
return (this);
}
}

View File

@ -24,11 +24,13 @@ package com.kingsrook.qqq.backend.core.model.metadata.tables;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizer;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
import com.kingsrook.qqq.backend.core.model.data.QRecordEntityField;
@ -84,6 +86,17 @@ public class QTableMetaData implements QAppChildMetaData, Serializable
/*******************************************************************************
**
*******************************************************************************/
@Override
public String toString()
{
return ("QTableMetaData[" + name + "]");
}
/*******************************************************************************
**
*******************************************************************************/
@ -408,12 +421,7 @@ public class QTableMetaData implements QAppChildMetaData, Serializable
}
QCodeReference function = customizers.get(customizerName);
if(function == null)
{
throw (new IllegalArgumentException("Customizer [" + customizerName + "] was not found in table [" + name + "]."));
}
return (Optional.of(function));
return (Optional.ofNullable(function));
}
@ -455,6 +463,16 @@ public class QTableMetaData implements QAppChildMetaData, Serializable
/*******************************************************************************
**
*******************************************************************************/
public QTableMetaData withCustomizer(TableCustomizer tableCustomizer, QCodeReference customizer)
{
return (withCustomizer(tableCustomizer.getRole(), customizer));
}
/*******************************************************************************
**
*******************************************************************************/
@ -592,6 +610,18 @@ public class QTableMetaData implements QAppChildMetaData, Serializable
/*******************************************************************************
** Fluent setter for recordLabelFields
**
*******************************************************************************/
public QTableMetaData withRecordLabelFields(String... recordLabelFields)
{
this.recordLabelFields = Arrays.asList(recordLabelFields);
return (this);
}
/*******************************************************************************
** Getter for sections
**

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

@ -72,6 +72,7 @@ public class QBackendModuleDispatcher
// todo - let modules somehow "export" their types here?
// e.g., backend-core shouldn't need to "know" about the modules.
"com.kingsrook.qqq.backend.core.modules.backend.implementations.mock.MockBackendModule",
"com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryBackendModule",
"com.kingsrook.qqq.backend.module.rdbms.RDBMSBackendModule",
"com.kingsrook.qqq.backend.module.filesystem.local.FilesystemBackendModule",
"com.kingsrook.qqq.backend.module.filesystem.s3.S3BackendModule"

View File

@ -0,0 +1,116 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.modules.backend.implementations.memory;
import com.kingsrook.qqq.backend.core.actions.interfaces.CountInterface;
import com.kingsrook.qqq.backend.core.actions.interfaces.DeleteInterface;
import com.kingsrook.qqq.backend.core.actions.interfaces.InsertInterface;
import com.kingsrook.qqq.backend.core.actions.interfaces.QueryInterface;
import com.kingsrook.qqq.backend.core.actions.interfaces.UpdateInterface;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
/*******************************************************************************
** A simple (probably only valid for testing?) implementation of the QModuleInterface,
** that just stores its records in-memory.
**
*******************************************************************************/
public class MemoryBackendModule implements QBackendModuleInterface
{
/*******************************************************************************
** Method where a backend module must be able to provide its type (name).
*******************************************************************************/
@Override
public String getBackendType()
{
return ("memory");
}
/*******************************************************************************
** Method to identify the class used for backend meta data for this module.
*******************************************************************************/
@Override
public Class<? extends QBackendMetaData> getBackendMetaDataClass()
{
return (QBackendMetaData.class);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public CountInterface getCountInterface()
{
return new MemoryCountAction();
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public QueryInterface getQueryInterface()
{
return new MemoryQueryAction();
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public InsertInterface getInsertInterface()
{
return (new MemoryInsertAction());
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public UpdateInterface getUpdateInterface()
{
return (new MemoryUpdateAction());
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public DeleteInterface getDeleteInterface()
{
return (new MemoryDeleteAction());
}
}

View File

@ -0,0 +1,54 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.modules.backend.implementations.memory;
import com.kingsrook.qqq.backend.core.actions.interfaces.CountInterface;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput;
/*******************************************************************************
** In-memory version of count action.
**
*******************************************************************************/
public class MemoryCountAction implements CountInterface
{
/*******************************************************************************
**
*******************************************************************************/
public CountOutput execute(CountInput countInput) throws QException
{
try
{
CountOutput countOutput = new CountOutput();
countOutput.setCount(MemoryRecordStore.getInstance().count(countInput));
return (countOutput);
}
catch(Exception e)
{
throw new QException("Error executing count", e);
}
}
}

View File

@ -0,0 +1,55 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.modules.backend.implementations.memory;
import com.kingsrook.qqq.backend.core.actions.interfaces.DeleteInterface;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteOutput;
/*******************************************************************************
** In-memory version of delete action.
**
*******************************************************************************/
public class MemoryDeleteAction implements DeleteInterface
{
/*******************************************************************************
**
*******************************************************************************/
public DeleteOutput execute(DeleteInput deleteInput) throws QException
{
try
{
DeleteOutput deleteOutput = new DeleteOutput();
deleteOutput.setDeletedRecordCount(MemoryRecordStore.getInstance().delete(deleteInput));
return (deleteOutput);
}
catch(Exception e)
{
throw new QException("Error executing delete: " + e.getMessage(), e);
}
}
}

View File

@ -0,0 +1,55 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.modules.backend.implementations.memory;
import com.kingsrook.qqq.backend.core.actions.interfaces.InsertInterface;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput;
/*******************************************************************************
** In-memory version of insert action.
**
*******************************************************************************/
public class MemoryInsertAction implements InsertInterface
{
/*******************************************************************************
**
*******************************************************************************/
public InsertOutput execute(InsertInput insertInput) throws QException
{
try
{
InsertOutput insertOutput = new InsertOutput();
insertOutput.setRecords(MemoryRecordStore.getInstance().insert(insertInput, true));
return (insertOutput);
}
catch(Exception e)
{
throw new QException("Error executing insert: " + e.getMessage(), e);
}
}
}

View File

@ -0,0 +1,55 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.modules.backend.implementations.memory;
import com.kingsrook.qqq.backend.core.actions.interfaces.QueryInterface;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
/*******************************************************************************
** In-memory version of query action.
**
*******************************************************************************/
public class MemoryQueryAction implements QueryInterface
{
/*******************************************************************************
**
*******************************************************************************/
public QueryOutput execute(QueryInput queryInput) throws QException
{
try
{
QueryOutput queryOutput = new QueryOutput(queryInput);
queryOutput.addRecords(MemoryRecordStore.getInstance().query(queryInput));
return (queryOutput);
}
catch(Exception e)
{
throw new QException("Error executing query", e);
}
}
}

View File

@ -0,0 +1,360 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.modules.backend.implementations.memory;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import org.apache.commons.lang.NotImplementedException;
/*******************************************************************************
** Storage provider for the MemoryBackendModule
*******************************************************************************/
public class MemoryRecordStore
{
private static MemoryRecordStore instance;
private Map<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";
/*******************************************************************************
** private singleton constructor
*******************************************************************************/
private MemoryRecordStore()
{
data = new HashMap<>();
nextSerials = new HashMap<>();
}
/*******************************************************************************
** Forget all data in the memory store...
*******************************************************************************/
public void reset()
{
data.clear();
nextSerials.clear();
}
/*******************************************************************************
** singleton accessor
*******************************************************************************/
public static MemoryRecordStore getInstance()
{
if(instance == null)
{
instance = new MemoryRecordStore();
}
return (instance);
}
/*******************************************************************************
**
*******************************************************************************/
private Map<Serializable, QRecord> getTableData(QTableMetaData table)
{
if(!data.containsKey(table.getName()))
{
data.put(table.getName(), new HashMap<>());
}
return (data.get(table.getName()));
}
/*******************************************************************************
**
*******************************************************************************/
public List<QRecord> query(QueryInput input)
{
incrementStatistic(STAT_QUERIES_RAN);
Map<Serializable, QRecord> tableData = getTableData(input.getTable());
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);
}
/*******************************************************************************
**
*******************************************************************************/
public Integer count(CountInput input)
{
QueryInput queryInput = new QueryInput(input.getInstance());
queryInput.setSession(input.getSession());
queryInput.setTableName(input.getTableName());
queryInput.setFilter(input.getFilter());
List<QRecord> queryResult = query(queryInput);
return (queryResult.size());
}
/*******************************************************************************
**
*******************************************************************************/
public List<QRecord> insert(InsertInput input, boolean returnInsertedRecords)
{
if(input.getRecords() == null)
{
return (new ArrayList<>());
}
QTableMetaData table = input.getTable();
Map<Serializable, QRecord> tableData = getTableData(table);
////////////////////////////////////////
// grab the next unique serial to use //
////////////////////////////////////////
Integer nextSerial = nextSerials.get(table.getName());
if(nextSerial == null)
{
nextSerial = 1;
}
while(tableData.containsKey(nextSerial))
{
nextSerial++;
}
List<QRecord> outputRecords = new ArrayList<>();
QFieldMetaData primaryKeyField = table.getField(table.getPrimaryKeyField());
for(QRecord record : input.getRecords())
{
/////////////////////////////////////////////////
// set the next serial in the record if needed //
/////////////////////////////////////////////////
if(record.getValue(primaryKeyField.getName()) == null && primaryKeyField.getType().equals(QFieldType.INTEGER))
{
record.setValue(primaryKeyField.getName(), nextSerial++);
}
///////////////////////////////////////////////////////////////////////////////////////////////////
// make sure that if the user supplied a serial, greater than the one we had, that we skip ahead //
///////////////////////////////////////////////////////////////////////////////////////////////////
if(primaryKeyField.getType().equals(QFieldType.INTEGER) && record.getValueInteger(primaryKeyField.getName()) > nextSerial)
{
nextSerial = record.getValueInteger(primaryKeyField.getName()) + 1;
}
tableData.put(record.getValue(primaryKeyField.getName()), record);
if(returnInsertedRecords)
{
outputRecords.add(record);
}
}
nextSerials.put(table.getName(), nextSerial);
return (outputRecords);
}
/*******************************************************************************
**
*******************************************************************************/
public List<QRecord> update(UpdateInput input, boolean returnUpdatedRecords)
{
if(input.getRecords() == null)
{
return (new ArrayList<>());
}
QTableMetaData table = input.getTable();
Map<Serializable, QRecord> tableData = getTableData(table);
List<QRecord> outputRecords = new ArrayList<>();
QFieldMetaData primaryKeyField = table.getField(table.getPrimaryKeyField());
for(QRecord record : input.getRecords())
{
Serializable primaryKeyValue = record.getValue(primaryKeyField.getName());
if(tableData.containsKey(primaryKeyValue))
{
QRecord recordToUpdate = tableData.get(primaryKeyValue);
for(Map.Entry<String, Serializable> valueEntry : record.getValues().entrySet())
{
recordToUpdate.setValue(valueEntry.getKey(), valueEntry.getValue());
}
if(returnUpdatedRecords)
{
outputRecords.add(record);
}
}
}
return (outputRecords);
}
/*******************************************************************************
**
*******************************************************************************/
public int delete(DeleteInput input)
{
if(input.getPrimaryKeys() == null)
{
return (0);
}
QTableMetaData table = input.getTable();
Map<Serializable, QRecord> tableData = getTableData(table);
int rowsDeleted = 0;
for(Serializable primaryKeyValue : input.getPrimaryKeys())
{
if(tableData.containsKey(primaryKeyValue))
{
tableData.remove(primaryKeyValue);
rowsDeleted++;
}
}
return (rowsDeleted);
}
/*******************************************************************************
** Setter for collectStatistics
**
*******************************************************************************/
public static void setCollectStatistics(boolean collectStatistics)
{
MemoryRecordStore.collectStatistics = collectStatistics;
}
/*******************************************************************************
** Increment a statistic
**
*******************************************************************************/
public static void incrementStatistic(String statName)
{
if(collectStatistics)
{
statistics.putIfAbsent(statName, 0);
statistics.put(statName, statistics.get(statName) + 1);
}
}
/*******************************************************************************
** clear the map of statistics
**
*******************************************************************************/
public static void resetStatistics()
{
statistics.clear();
}
/*******************************************************************************
** Getter for statistics
**
*******************************************************************************/
public static Map<String, Integer> getStatistics()
{
return statistics;
}
}

View File

@ -0,0 +1,55 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.modules.backend.implementations.memory;
import com.kingsrook.qqq.backend.core.actions.interfaces.UpdateInterface;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateOutput;
/*******************************************************************************
** In-memory version of update action.
**
*******************************************************************************/
public class MemoryUpdateAction implements UpdateInterface
{
/*******************************************************************************
**
*******************************************************************************/
public UpdateOutput execute(UpdateInput updateInput) throws QException
{
try
{
UpdateOutput updateOutput = new UpdateOutput();
updateOutput.setRecords(MemoryRecordStore.getInstance().update(updateInput, true));
return (updateOutput);
}
catch(Exception e)
{
throw new QException("Error executing update: " + e.getMessage(), e);
}
}
}

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
{
@ -422,7 +423,7 @@ public class ValueUtils
/*******************************************************************************
**
*******************************************************************************/
public static Object getValueAsLocalTime(Serializable value)
public static LocalTime getValueAsLocalTime(Serializable value)
{
try
{

View File

@ -22,6 +22,8 @@
package com.kingsrook.qqq.backend.core.actions.tables;
import java.util.List;
import com.kingsrook.qqq.backend.core.actions.reporting.RecordPipe;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
@ -47,18 +49,79 @@ class QueryActionTest
@Test
public void test() throws QException
{
QueryInput request = new QueryInput(TestUtils.defineInstance());
request.setSession(TestUtils.getMockSession());
request.setTableName("person");
QueryOutput result = new QueryAction().execute(request);
assertNotNull(result);
QueryInput queryInput = new QueryInput(TestUtils.defineInstance());
queryInput.setSession(TestUtils.getMockSession());
queryInput.setTableName("person");
QueryOutput queryOutput = new QueryAction().execute(queryInput);
assertNotNull(queryOutput);
assertThat(result.getRecords()).isNotEmpty();
for(QRecord record : result.getRecords())
assertThat(queryOutput.getRecords()).isNotEmpty();
for(QRecord record : queryOutput.getRecords())
{
assertThat(record.getValues()).isNotEmpty();
assertThat(record.getDisplayValues()).isNotEmpty();
assertThat(record.getErrors()).isEmpty();
///////////////////////////////////////////////////////////////
// this SHOULD be empty, based on the default for the should //
///////////////////////////////////////////////////////////////
assertThat(record.getDisplayValues()).isEmpty();
}
////////////////////////////////////
// now flip that field and re-run //
////////////////////////////////////
queryInput.setShouldGenerateDisplayValues(true);
assertThat(queryOutput.getRecords()).isNotEmpty();
queryOutput = new QueryAction().execute(queryInput);
for(QRecord record : queryOutput.getRecords())
{
assertThat(record.getDisplayValues()).isNotEmpty();
}
}
/*******************************************************************************
** Test running with a recordPipe - using the shape table, which uses the memory
** backend, which is known to do an addAll to the query output.
**
*******************************************************************************/
@Test
public void testRecordPipeShapeTable() throws QException
{
TestUtils.insertDefaultShapes(TestUtils.defineInstance());
RecordPipe pipe = new RecordPipe();
QueryInput queryInput = new QueryInput(TestUtils.defineInstance());
queryInput.setSession(TestUtils.getMockSession());
queryInput.setTableName(TestUtils.TABLE_NAME_SHAPE);
queryInput.setRecordPipe(pipe);
QueryOutput queryOutput = new QueryAction().execute(queryInput);
assertNotNull(queryOutput);
List<QRecord> records = pipe.consumeAvailableRecords();
assertThat(records).isNotEmpty();
}
/*******************************************************************************
** Test running with a recordPipe - using the person table, which uses the mock
** backend, which is known to do a single-add (not addAll) to the query output.
**
*******************************************************************************/
@Test
public void testRecordPipePersonTable() throws QException
{
RecordPipe pipe = new RecordPipe();
QueryInput queryInput = new QueryInput(TestUtils.defineInstance());
queryInput.setSession(TestUtils.getMockSession());
queryInput.setTableName(TestUtils.TABLE_NAME_PERSON);
queryInput.setRecordPipe(pipe);
QueryOutput queryOutput = new QueryAction().execute(queryInput);
assertNotNull(queryOutput);
List<QRecord> records = pipe.consumeAvailableRecords();
assertThat(records).isNotEmpty();
}
}

View File

@ -0,0 +1,335 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <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.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.PVSValueFormatAndFields;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryRecordStore;
import com.kingsrook.qqq.backend.core.utils.TestUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
/*******************************************************************************
** Unit test for QPossibleValueTranslator
*******************************************************************************/
public class QPossibleValueTranslatorTest
{
/*******************************************************************************
**
*******************************************************************************/
@BeforeEach
void beforeEach()
{
MemoryRecordStore.getInstance().reset();
MemoryRecordStore.resetStatistics();
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testPossibleValueEnum()
{
QInstance qInstance = TestUtils.defineInstance();
QPossibleValueTranslator possibleValueTranslator = new QPossibleValueTranslator(qInstance, new QSession());
QFieldMetaData stateField = qInstance.getTable("person").getField("homeStateId");
QPossibleValueSource possibleValueSource = qInstance.getPossibleValueSource(stateField.getPossibleValueSourceName());
//////////////////////////////////////////////////////////////////////////
// assert the default formatting for a not-found value is a null string //
//////////////////////////////////////////////////////////////////////////
assertNull(possibleValueTranslator.translatePossibleValue(stateField, null));
assertNull(possibleValueTranslator.translatePossibleValue(stateField, -1));
//////////////////////////////////////////////////////////////////////
// let the not-found value be a simple string (no formatted values) //
//////////////////////////////////////////////////////////////////////
possibleValueSource.setValueFormatIfNotFound("?");
assertEquals("?", possibleValueTranslator.translatePossibleValue(stateField, null));
assertEquals("?", possibleValueTranslator.translatePossibleValue(stateField, -1));
/////////////////////////////////////////////////////////////
// let the not-found value be a string w/ formatted values //
/////////////////////////////////////////////////////////////
possibleValueSource.setValueFormatIfNotFound("? (%s)");
possibleValueSource.setValueFieldsIfNotFound(List.of("id"));
assertEquals("? ()", possibleValueTranslator.translatePossibleValue(stateField, null));
assertEquals("? (-1)", possibleValueTranslator.translatePossibleValue(stateField, -1));
/////////////////////////////////////////////////////
// assert the default formatting is just the label //
/////////////////////////////////////////////////////
assertEquals("MO", possibleValueTranslator.translatePossibleValue(stateField, 2));
assertEquals("IL", possibleValueTranslator.translatePossibleValue(stateField, 1));
/////////////////////////////////////////////////////////////////
// assert the LABEL_ONLY format (when called out specifically) //
/////////////////////////////////////////////////////////////////
possibleValueSource.setValueFormatAndFields(PVSValueFormatAndFields.LABEL_ONLY);
assertEquals("IL", possibleValueTranslator.translatePossibleValue(stateField, 1));
///////////////////////////////////////
// assert the LABEL_PARAMS_ID format //
///////////////////////////////////////
possibleValueSource.setValueFormatAndFields(PVSValueFormatAndFields.LABEL_PARENS_ID);
assertEquals("IL (1)", possibleValueTranslator.translatePossibleValue(stateField, 1));
//////////////////////////////////////
// assert the ID_COLON_LABEL format //
//////////////////////////////////////
possibleValueSource.setValueFormat(PVSValueFormatAndFields.ID_COLON_LABEL.getFormat());
possibleValueSource.setValueFields(PVSValueFormatAndFields.ID_COLON_LABEL.getFields());
assertEquals("1: IL", possibleValueTranslator.translatePossibleValue(stateField, 1));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testPossibleValueTable() throws QException
{
QInstance qInstance = TestUtils.defineInstance();
QPossibleValueTranslator possibleValueTranslator = new QPossibleValueTranslator(qInstance, new QSession());
QTableMetaData shapeTable = qInstance.getTable(TestUtils.TABLE_NAME_SHAPE);
QFieldMetaData shapeField = qInstance.getTable(TestUtils.TABLE_NAME_PERSON).getField("favoriteShapeId");
QPossibleValueSource possibleValueSource = qInstance.getPossibleValueSource(shapeField.getPossibleValueSourceName());
TestUtils.insertDefaultShapes(qInstance);
//////////////////////////////////////////////////////////////////////////
// assert the default formatting for a not-found value is a null string //
//////////////////////////////////////////////////////////////////////////
assertNull(possibleValueTranslator.translatePossibleValue(shapeField, null));
assertNull(possibleValueTranslator.translatePossibleValue(shapeField, -1));
//////////////////////////////////////////////////////////////////////
// let the not-found value be a simple string (no formatted values) //
//////////////////////////////////////////////////////////////////////
possibleValueSource.setValueFormatIfNotFound("?");
assertEquals("?", possibleValueTranslator.translatePossibleValue(shapeField, null));
assertEquals("?", possibleValueTranslator.translatePossibleValue(shapeField, -1));
/////////////////////////////////////////////////////
// assert the default formatting is just the label //
/////////////////////////////////////////////////////
assertEquals("Square", possibleValueTranslator.translatePossibleValue(shapeField, 2));
assertEquals("Triangle", possibleValueTranslator.translatePossibleValue(shapeField, 1));
///////////////////////////////////////
// assert the LABEL_PARAMS_ID format //
///////////////////////////////////////
possibleValueSource.setValueFormatAndFields(PVSValueFormatAndFields.LABEL_PARENS_ID);
assertEquals("Circle (3)", possibleValueTranslator.translatePossibleValue(shapeField, 3));
///////////////////////////////////////////////////////////
// assert that we don't re-run queries for cached values //
///////////////////////////////////////////////////////////
possibleValueTranslator = new QPossibleValueTranslator(qInstance, new QSession());
MemoryRecordStore.setCollectStatistics(true);
possibleValueTranslator.translatePossibleValue(shapeField, 1);
possibleValueTranslator.translatePossibleValue(shapeField, 2);
assertEquals(2, MemoryRecordStore.getStatistics().get(MemoryRecordStore.STAT_QUERIES_RAN), "Should have ran 2 queries so far");
possibleValueTranslator.translatePossibleValue(shapeField, 2);
possibleValueTranslator.translatePossibleValue(shapeField, 3);
possibleValueTranslator.translatePossibleValue(shapeField, 3);
assertEquals(3, MemoryRecordStore.getStatistics().get(MemoryRecordStore.STAT_QUERIES_RAN), "Should have ran 3 queries in total");
///////////////////////////////////////////////////////////////
// assert that if we prime the cache, we can do just 1 query //
///////////////////////////////////////////////////////////////
possibleValueTranslator = new QPossibleValueTranslator(qInstance, new QSession());
List<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");
}
/*******************************************************************************
** Make sure that if we have 2 different PVS's pointed at the same 1 table,
** that we avoid re-doing queries, and that we actually get different (formatted) values.
*******************************************************************************/
@Test
void testPossibleValueTableMultiplePvsForATable() throws QException
{
QInstance qInstance = TestUtils.defineInstance();
QTableMetaData shapeTable = qInstance.getTable(TestUtils.TABLE_NAME_SHAPE);
QTableMetaData personTable = qInstance.getTable(TestUtils.TABLE_NAME_PERSON);
////////////////////////////////////////////////////////////////////
// define a second version of the Shape PVS, with a unique format //
////////////////////////////////////////////////////////////////////
qInstance.addPossibleValueSource(new QPossibleValueSource()
.withName("shapeV2")
.withType(QPossibleValueSourceType.TABLE)
.withTableName(TestUtils.TABLE_NAME_SHAPE)
.withValueFormat("%d: %s")
.withValueFields(List.of("id", "label"))
);
//////////////////////////////////////////////////////
// use that PVS in a new column on the person table //
//////////////////////////////////////////////////////
personTable.addField(new QFieldMetaData("currentShapeId", QFieldType.INTEGER)
.withPossibleValueSourceName("shapeV2")
);
TestUtils.insertDefaultShapes(qInstance);
///////////////////////////////////////////////////////
// define a list of persons pointing at those shapes //
///////////////////////////////////////////////////////
List<QRecord> personRecords = List.of(
new QRecord().withTableName(TestUtils.TABLE_NAME_PERSON).withValue("favoriteShapeId", 1).withValue("currentShapeId", 2),
new QRecord().withTableName(TestUtils.TABLE_NAME_PERSON).withValue("favoriteShapeId", 1).withValue("currentShapeId", 3),
new QRecord().withTableName(TestUtils.TABLE_NAME_PERSON).withValue("favoriteShapeId", 2).withValue("currentShapeId", 3),
new QRecord().withTableName(TestUtils.TABLE_NAME_PERSON).withValue("favoriteShapeId", 2).withValue("currentShapeId", 3)
);
/////////////////////////
// translate the PVS's //
/////////////////////////
MemoryRecordStore.setCollectStatistics(true);
new QPossibleValueTranslator(qInstance, new QSession()).translatePossibleValuesInRecords(personTable, personRecords);
/////////////////////////////////
// assert only 1 query was ran //
/////////////////////////////////
assertEquals(1, MemoryRecordStore.getStatistics().get(MemoryRecordStore.STAT_QUERIES_RAN), "Should only run 1 query");
////////////////////////////////////////
// assert expected values and formats //
////////////////////////////////////////
assertEquals("Triangle", personRecords.get(0).getDisplayValue("favoriteShapeId"));
assertEquals("2: Square", personRecords.get(0).getDisplayValue("currentShapeId"));
assertEquals("Triangle", personRecords.get(1).getDisplayValue("favoriteShapeId"));
assertEquals("3: Circle", personRecords.get(1).getDisplayValue("currentShapeId"));
assertEquals("Square", personRecords.get(2).getDisplayValue("favoriteShapeId"));
assertEquals("3: Circle", personRecords.get(2).getDisplayValue("currentShapeId"));
}
/*******************************************************************************
** Make sure that if we have 2 different PVS's pointed at the same 1 table,
** that we avoid re-doing queries, and that we actually get different (formatted) values.
*******************************************************************************/
@Test
void testCustomPossibleValue() throws QException
{
QInstance qInstance = TestUtils.defineInstance();
QTableMetaData personTable = qInstance.getTable(TestUtils.TABLE_NAME_PERSON);
String fieldName = "customValue";
//////////////////////////////////////////////////////////////
// define a list of persons with values in the custom field //
//////////////////////////////////////////////////////////////
List<QRecord> personRecords = List.of(
new QRecord().withTableName(TestUtils.TABLE_NAME_PERSON).withValue(fieldName, 1),
new QRecord().withTableName(TestUtils.TABLE_NAME_PERSON).withValue(fieldName, 2),
new QRecord().withTableName(TestUtils.TABLE_NAME_PERSON).withValue(fieldName, "Buckle my shoe")
);
/////////////////////////
// translate the PVS's //
/////////////////////////
new QPossibleValueTranslator(qInstance, new QSession()).translatePossibleValuesInRecords(personTable, personRecords);
////////////////////////////////////////
// assert expected values and formats //
////////////////////////////////////////
assertEquals("Custom[1]", personRecords.get(0).getDisplayValue(fieldName));
assertEquals("Custom[2]", personRecords.get(1).getDisplayValue(fieldName));
assertEquals("Custom[Buckle my shoe]", personRecords.get(2).getDisplayValue(fieldName));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testSetDisplayValuesInRecords()
{
QTableMetaData table = TestUtils.defineTablePerson();
/////////////////////////////////////////////////////////////////
// first, make sure it doesn't crash with null or empty inputs //
/////////////////////////////////////////////////////////////////
QPossibleValueTranslator possibleValueTranslator = new QPossibleValueTranslator(TestUtils.defineInstance(), new QSession());
possibleValueTranslator.translatePossibleValuesInRecords(table, null);
possibleValueTranslator.translatePossibleValuesInRecords(table, Collections.emptyList());
List<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,65 @@ class CsvToQRecordAdapterTest
// todo - this is what the method header comment means when it says we don't handle all cases well...
// Assertions.assertEquals(List.of("A", "B", "C", "C 2", "C 3"), csvToQRecordAdapter.makeHeadersUnique(List.of("A", "B", "C 2", "C", "C 3")));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testByteOrderMarker()
{
CsvToQRecordAdapter csvToQRecordAdapter = new CsvToQRecordAdapter();
//////////////////////////////////////////////////////////////////////////////////////////////////////////
// note - there's a zero-width non-breaking-space character (0xFEFF or some-such) //
// at the start of this string!! You may not be able to see it, depending on where you view this file. //
//////////////////////////////////////////////////////////////////////////////////////////////////////////
List<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,21 @@ class QInstanceEnricherTest
/*******************************************************************************
**
*******************************************************************************/
@Test
void testNameToLabel()
{
assertEquals("Address 2", QInstanceEnricher.nameToLabel("address2"));
assertEquals("Field 20", QInstanceEnricher.nameToLabel("field20"));
assertEquals("Something USA", QInstanceEnricher.nameToLabel("somethingUSA"));
assertEquals("Number 1 Dad", QInstanceEnricher.nameToLabel("number1Dad"));
assertEquals("Number 417 Dad", QInstanceEnricher.nameToLabel("number417Dad"));
}
/*******************************************************************************
**
*******************************************************************************/
@ -146,4 +161,28 @@ class QInstanceEnricherTest
assertEquals("tla_and_another_tla", QInstanceEnricher.inferBackendName("TLAAndAnotherTLA"));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testInferredRecordLabelFormat()
{
QInstance qInstance = TestUtils.defineInstance();
QTableMetaData table = qInstance.getTable("person").withRecordLabelFormat(null).withRecordLabelFields(new ArrayList<>());
new QInstanceEnricher().enrich(qInstance);
assertNull(table.getRecordLabelFormat());
qInstance = TestUtils.defineInstance();
table = qInstance.getTable("person").withRecordLabelFormat(null).withRecordLabelFields("firstName");
new QInstanceEnricher().enrich(qInstance);
assertEquals("%s", table.getRecordLabelFormat());
qInstance = TestUtils.defineInstance();
table = qInstance.getTable("person").withRecordLabelFormat(null).withRecordLabelFields("firstName", "lastName");
new QInstanceEnricher().enrich(qInstance);
assertEquals("%s %s", table.getRecordLabelFormat());
}
}

View File

@ -22,16 +22,25 @@
package com.kingsrook.qqq.backend.core.instances;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeUsage;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValue;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier;
@ -115,7 +124,8 @@ class QInstanceValidatorTest
qInstance.setTables(null);
qInstance.setProcesses(null);
},
"At least 1 table must be defined");
"At least 1 table must be defined",
"Unrecognized table shape for possibleValueSource shape");
}
@ -132,7 +142,8 @@ class QInstanceValidatorTest
qInstance.setTables(new HashMap<>());
qInstance.setProcesses(new HashMap<>());
},
"At least 1 table must be defined");
"At least 1 table must be defined",
"Unrecognized table shape for possibleValueSource shape");
}
@ -150,10 +161,13 @@ class QInstanceValidatorTest
qInstance.getTable("person").setName("notPerson");
qInstance.getBackend("default").setName("notDefault");
qInstance.getProcess(TestUtils.PROCESS_NAME_GREET_PEOPLE).setName("notGreetPeople");
qInstance.getPossibleValueSource(TestUtils.POSSIBLE_VALUE_SOURCE_STATE).setName("notStates");
},
"Inconsistent naming for table",
"Inconsistent naming for backend",
"Inconsistent naming for process");
"Inconsistent naming for process",
"Inconsistent naming for possibleValueSource"
);
}
@ -184,6 +198,18 @@ class QInstanceValidatorTest
/*******************************************************************************
**
*******************************************************************************/
@Test
public void test_validateTableBadRecordFormatField()
{
assertValidationFailureReasons((qInstance) -> qInstance.getTable("person").withRecordLabelFields("notAField"),
"not a field");
}
/*******************************************************************************
** Test that if a process specifies a table that doesn't exist, that it fails.
**
@ -245,6 +271,138 @@ class QInstanceValidatorTest
/*******************************************************************************
**
*******************************************************************************/
@Test
void testTableCustomizers()
{
assertValidationFailureReasons((qInstance) -> qInstance.getTable("person").withCustomizer(TableCustomizers.POST_QUERY_RECORD.getRole(), new QCodeReference()),
"missing a code reference name", "missing a code type");
assertValidationFailureReasons((qInstance) -> qInstance.getTable("person").withCustomizer(TableCustomizers.POST_QUERY_RECORD.getRole(), new QCodeReference(null, QCodeType.JAVA, null)),
"missing a code reference name");
assertValidationFailureReasons((qInstance) -> qInstance.getTable("person").withCustomizer(TableCustomizers.POST_QUERY_RECORD.getRole(), new QCodeReference("", QCodeType.JAVA, null)),
"missing a code reference name");
assertValidationFailureReasons((qInstance) -> qInstance.getTable("person").withCustomizer(TableCustomizers.POST_QUERY_RECORD.getRole(), new QCodeReference("Test", null, null)),
"missing a code type");
assertValidationFailureReasons((qInstance) -> qInstance.getTable("person").withCustomizer(TableCustomizers.POST_QUERY_RECORD.getRole(), new QCodeReference("Test", QCodeType.JAVA, QCodeUsage.CUSTOMIZER)),
"Class for CodeReference could not be found");
assertValidationFailureReasons((qInstance) -> qInstance.getTable("person").withCustomizer(TableCustomizers.POST_QUERY_RECORD.getRole(), new QCodeReference(CustomizerWithNoVoidConstructor.class, QCodeUsage.CUSTOMIZER)),
"Instance of CodeReference could not be created");
assertValidationFailureReasons((qInstance) -> qInstance.getTable("person").withCustomizer(TableCustomizers.POST_QUERY_RECORD.getRole(), new QCodeReference(CustomizerThatIsNotAFunction.class, QCodeUsage.CUSTOMIZER)),
"CodeReference could not be casted");
assertValidationFailureReasons((qInstance) -> qInstance.getTable("person").withCustomizer(TableCustomizers.POST_QUERY_RECORD.getRole(), new QCodeReference(CustomizerFunctionWithIncorrectTypeParameters.class, QCodeUsage.CUSTOMIZER)),
"Error validating customizer type parameters");
assertValidationFailureReasons((qInstance) -> qInstance.getTable("person").withCustomizer(TableCustomizers.POST_QUERY_RECORD.getRole(), new QCodeReference(CustomizerFunctionWithIncorrectTypeParameter1.class, QCodeUsage.CUSTOMIZER)),
"Error validating customizer type parameters");
assertValidationFailureReasons((qInstance) -> qInstance.getTable("person").withCustomizer(TableCustomizers.POST_QUERY_RECORD.getRole(), new QCodeReference(CustomizerFunctionWithIncorrectTypeParameter2.class, QCodeUsage.CUSTOMIZER)),
"Error validating customizer type parameters");
assertValidationSuccess((qInstance) -> qInstance.getTable("person").withCustomizer(TableCustomizers.POST_QUERY_RECORD.getRole(), new QCodeReference(CustomizerValid.class, QCodeUsage.CUSTOMIZER)));
}
/*******************************************************************************
**
*******************************************************************************/
public static class CustomizerWithNoVoidConstructor
{
public CustomizerWithNoVoidConstructor(boolean b)
{
}
}
/*******************************************************************************
**
*******************************************************************************/
public static class CustomizerWithOnlyPrivateConstructor
{
private CustomizerWithOnlyPrivateConstructor()
{
}
}
/*******************************************************************************
**
*******************************************************************************/
public static class CustomizerThatIsNotAFunction
{
}
/*******************************************************************************
**
*******************************************************************************/
public static class CustomizerFunctionWithIncorrectTypeParameters implements Function<String, String>
{
@Override
public String apply(String s)
{
return null;
}
}
/*******************************************************************************
**
*******************************************************************************/
public static class CustomizerFunctionWithIncorrectTypeParameter1 implements Function<String, QRecord>
{
@Override
public QRecord apply(String s)
{
return null;
}
}
/*******************************************************************************
**
*******************************************************************************/
public static class CustomizerFunctionWithIncorrectTypeParameter2 implements Function<QRecord, String>
{
@Override
public String apply(QRecord s)
{
return "Test";
}
}
/*******************************************************************************
**
*******************************************************************************/
public static class CustomizerValid implements Function<QRecord, QRecord>
{
@Override
public QRecord apply(QRecord record)
{
return null;
}
}
/*******************************************************************************
** Test that if a field specifies a backend that doesn't exist, that it fails.
**
@ -252,7 +410,7 @@ class QInstanceValidatorTest
@Test
public void test_validateFieldWithMissingPossibleValueSource()
{
assertValidationFailureReasons((qInstance) -> qInstance.getTable("person").getField("homeState").setPossibleValueSourceName("not a real possible value source"),
assertValidationFailureReasons((qInstance) -> qInstance.getTable("person").getField("homeStateId").setPossibleValueSourceName("not a real possible value source"),
"Unrecognized possibleValueSourceName");
}
@ -319,6 +477,7 @@ class QInstanceValidatorTest
}
/*******************************************************************************
**
*******************************************************************************/
@ -376,6 +535,7 @@ class QInstanceValidatorTest
}
/*******************************************************************************
**
*******************************************************************************/
@ -391,6 +551,7 @@ class QInstanceValidatorTest
}
/*******************************************************************************
**
*******************************************************************************/
@ -408,6 +569,86 @@ class QInstanceValidatorTest
/*******************************************************************************
**
*******************************************************************************/
@Test
void testPossibleValueSourceMissingType()
{
assertValidationFailureReasons((qInstance) -> qInstance.getPossibleValueSource(TestUtils.POSSIBLE_VALUE_SOURCE_STATE).setType(null),
"Missing type for possibleValueSource");
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testPossibleValueSourceMisConfiguredEnum()
{
assertValidationFailureReasons((qInstance) -> {
QPossibleValueSource possibleValueSource = qInstance.getPossibleValueSource(TestUtils.POSSIBLE_VALUE_SOURCE_STATE);
possibleValueSource.setTableName("person");
possibleValueSource.setCustomCodeReference(new QCodeReference());
possibleValueSource.setEnumValues(null);
},
"should not have a tableName",
"should not have a customCodeReference",
"is missing enum values");
assertValidationFailureReasons((qInstance) -> qInstance.getPossibleValueSource(TestUtils.POSSIBLE_VALUE_SOURCE_STATE).setEnumValues(new ArrayList<>()),
"is missing enum values");
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testPossibleValueSourceMisConfiguredTable()
{
assertValidationFailureReasons((qInstance) -> {
QPossibleValueSource possibleValueSource = qInstance.getPossibleValueSource(TestUtils.POSSIBLE_VALUE_SOURCE_SHAPE);
possibleValueSource.setTableName(null);
possibleValueSource.setCustomCodeReference(new QCodeReference());
possibleValueSource.setEnumValues(List.of(new QPossibleValue<>("test")));
},
"should not have enum values",
"should not have a customCodeReference",
"is missing a tableName");
assertValidationFailureReasons((qInstance) -> qInstance.getPossibleValueSource(TestUtils.POSSIBLE_VALUE_SOURCE_SHAPE).setTableName("Not a table"),
"Unrecognized table");
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testPossibleValueSourceMisConfiguredCustom()
{
assertValidationFailureReasons((qInstance) -> {
QPossibleValueSource possibleValueSource = qInstance.getPossibleValueSource(TestUtils.POSSIBLE_VALUE_SOURCE_CUSTOM);
possibleValueSource.setTableName("person");
possibleValueSource.setCustomCodeReference(null);
possibleValueSource.setEnumValues(List.of(new QPossibleValue<>("test")));
},
"should not have enum values",
"should not have a tableName",
"is missing a customCodeReference");
assertValidationFailureReasons((qInstance) -> qInstance.getPossibleValueSource(TestUtils.POSSIBLE_VALUE_SOURCE_CUSTOM).setCustomCodeReference(new QCodeReference()),
"not a possibleValueProvider",
"missing a code reference name",
"missing a code type");
}
/*******************************************************************************
** Run a little setup code on a qInstance; then validate it, and assert that it
** failed validation with reasons that match the supplied vararg-reasons (but allow
@ -448,7 +689,8 @@ class QInstanceValidatorTest
{
if(!allowExtraReasons)
{
assertEquals(reasons.length, e.getReasons().size(), "Expected number of validation failure reasons\nExpected: " + String.join(",", reasons) + "\nActual: " + e.getReasons());
int noOfReasons = e.getReasons() == null ? 0 : e.getReasons().size();
assertEquals(reasons.length, noOfReasons, "Expected number of validation failure reasons.\nExpected reasons: " + String.join(",", reasons) + "\nActual reasons: " + e.getReasons());
}
for(String reason : reasons)
@ -460,6 +702,25 @@ class QInstanceValidatorTest
/*******************************************************************************
** Assert that an instance is valid!
*******************************************************************************/
private void assertValidationSuccess(Consumer<QInstance> setup)
{
try
{
QInstance qInstance = TestUtils.defineInstance();
setup.accept(qInstance);
new QInstanceValidator().validate(qInstance);
}
catch(QInstanceValidationException e)
{
fail("Expected no validation errors, but received: " + e.getMessage());
}
}
/*******************************************************************************
** utility method for asserting that a specific reason string is found within
** the list of reasons in the QInstanceValidationException.

View File

@ -0,0 +1,334 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.modules.backend.implementations.memory;
import java.util.List;
import java.util.function.Function;
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
import com.kingsrook.qqq.backend.core.actions.tables.CountAction;
import com.kingsrook.qqq.backend.core.actions.tables.DeleteAction;
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeUsage;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.utils.TestUtils;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
/*******************************************************************************
** Unit test for MemoryBackendModule
*******************************************************************************/
class MemoryBackendModuleTest
{
/*******************************************************************************
**
*******************************************************************************/
@BeforeEach
@AfterEach
void beforeAndAfter()
{
MemoryRecordStore.getInstance().reset();
MemoryRecordStore.resetStatistics();
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testFullCRUD() throws QException
{
QInstance qInstance = TestUtils.defineInstance();
QTableMetaData table = qInstance.getTable(TestUtils.TABLE_NAME_SHAPE);
QSession session = new QSession();
/////////////////////////
// do an initial count //
/////////////////////////
CountInput countInput = new CountInput(qInstance);
countInput.setSession(session);
countInput.setTableName(table.getName());
assertEquals(0, new CountAction().execute(countInput).getCount());
//////////////////
// do an insert //
//////////////////
InsertInput insertInput = new InsertInput(qInstance);
insertInput.setSession(session);
insertInput.setTableName(table.getName());
insertInput.setRecords(getTestRecords(table));
InsertOutput insertOutput = new InsertAction().execute(insertInput);
assertEquals(3, insertOutput.getRecords().size());
assertTrue(insertOutput.getRecords().stream().allMatch(r -> r.getValue("id") != null));
assertTrue(insertOutput.getRecords().stream().anyMatch(r -> r.getValueInteger("id").equals(1)));
assertTrue(insertOutput.getRecords().stream().anyMatch(r -> r.getValueInteger("id").equals(2)));
assertTrue(insertOutput.getRecords().stream().anyMatch(r -> r.getValueInteger("id").equals(3)));
////////////////
// do a query //
////////////////
QueryInput queryInput = new QueryInput(qInstance);
queryInput.setSession(session);
queryInput.setTableName(table.getName());
QueryOutput queryOutput = new QueryAction().execute(queryInput);
assertEquals(3, queryOutput.getRecords().size());
assertTrue(queryOutput.getRecords().stream().allMatch(r -> r.getValue("id") != null));
assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueInteger("id").equals(1)));
assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueInteger("id").equals(2)));
assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueInteger("id").equals(3)));
assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueString("name").equals("My Triangle")));
assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueString("name").equals("Your Square")));
assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueString("name").equals("Some Circle")));
assertEquals(3, new CountAction().execute(countInput).getCount());
//////////////////
// do an update //
//////////////////
UpdateInput updateInput = new UpdateInput(qInstance);
updateInput.setSession(session);
updateInput.setTableName(table.getName());
updateInput.setRecords(List.of(
new QRecord()
.withTableName(table.getName())
.withValue("id", 1)
.withValue("name", "Not My Triangle any more"),
new QRecord()
.withTableName(table.getName())
.withValue("id", 3)
.withValue("type", "ellipse")
));
UpdateOutput updateOutput = new UpdateAction().execute(updateInput);
assertEquals(2, updateOutput.getRecords().size());
queryOutput = new QueryAction().execute(queryInput);
assertEquals(3, queryOutput.getRecords().size());
assertTrue(queryOutput.getRecords().stream().noneMatch(r -> r.getValueString("name").equals("My Triangle")));
assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueString("name").equals("Not My Triangle any more")));
assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueString("type").equals("ellipse")));
assertTrue(queryOutput.getRecords().stream().noneMatch(r -> r.getValueString("type").equals("circle")));
assertEquals(3, new CountAction().execute(countInput).getCount());
/////////////////////////
// do a filtered query //
/////////////////////////
queryInput = new QueryInput(qInstance);
queryInput.setSession(session);
queryInput.setTableName(table.getName());
queryInput.setFilter(new QQueryFilter().withCriteria(new QFilterCriteria("id", QCriteriaOperator.IN, List.of(1, 3))));
queryOutput = new QueryAction().execute(queryInput);
assertEquals(2, queryOutput.getRecords().size());
assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueInteger("id").equals(1)));
assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueInteger("id").equals(3)));
/////////////////////////
// do a filtered count //
/////////////////////////
countInput.setFilter(queryInput.getFilter());
assertEquals(2, new CountAction().execute(countInput).getCount());
/////////////////
// do a delete //
/////////////////
DeleteInput deleteInput = new DeleteInput(qInstance);
deleteInput.setSession(session);
deleteInput.setTableName(table.getName());
deleteInput.setPrimaryKeys(List.of(1, 2));
DeleteOutput deleteOutput = new DeleteAction().execute(deleteInput);
assertEquals(2, deleteOutput.getDeletedRecordCount());
assertEquals(1, new CountAction().execute(countInput).getCount());
queryOutput = new QueryAction().execute(queryInput);
assertEquals(1, queryOutput.getRecords().size());
assertTrue(queryOutput.getRecords().stream().noneMatch(r -> r.getValueInteger("id").equals(1)));
assertTrue(queryOutput.getRecords().stream().noneMatch(r -> r.getValueInteger("id").equals(2)));
assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueInteger("id").equals(3)));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testSerials() throws QException
{
QInstance qInstance = TestUtils.defineInstance();
QTableMetaData table = qInstance.getTable(TestUtils.TABLE_NAME_SHAPE);
QSession session = new QSession();
//////////////////
// do an insert //
//////////////////
InsertInput insertInput = new InsertInput(qInstance);
insertInput.setSession(session);
insertInput.setTableName(table.getName());
insertInput.setRecords(List.of(new QRecord().withTableName(table.getName()).withValue("name", "Shape 1")));
new InsertAction().execute(insertInput);
insertInput.setRecords(List.of(new QRecord().withTableName(table.getName()).withValue("name", "Shape 2")));
new InsertAction().execute(insertInput);
insertInput.setRecords(List.of(new QRecord().withTableName(table.getName()).withValue("name", "Shape 3")));
new InsertAction().execute(insertInput);
QueryInput queryInput = new QueryInput(qInstance);
queryInput.setSession(new QSession());
queryInput.setTableName(table.getName());
QueryOutput queryOutput = new QueryAction().execute(queryInput);
assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueInteger("id").equals(1)));
assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueInteger("id").equals(2)));
assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueInteger("id").equals(3)));
insertInput.setRecords(List.of(new QRecord().withTableName(table.getName()).withValue("id", 4).withValue("name", "Shape 4")));
new InsertAction().execute(insertInput);
queryOutput = new QueryAction().execute(queryInput);
assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueInteger("id").equals(4)));
insertInput.setRecords(List.of(new QRecord().withTableName(table.getName()).withValue("id", 6).withValue("name", "Shape 6")));
new InsertAction().execute(insertInput);
queryOutput = new QueryAction().execute(queryInput);
assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueInteger("id").equals(6)));
insertInput.setRecords(List.of(new QRecord().withTableName(table.getName()).withValue("name", "Shape 7")));
new InsertAction().execute(insertInput);
queryOutput = new QueryAction().execute(queryInput);
assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueInteger("id").equals(7)));
}
/*******************************************************************************
**
*******************************************************************************/
private List<QRecord> getTestRecords(QTableMetaData table)
{
return List.of(
new QRecord()
.withTableName(table.getName())
.withValue("name", "My Triangle")
.withValue("type", "triangle")
.withValue("noOfSides", 3)
.withValue("isPolygon", true),
new QRecord()
.withTableName(table.getName())
.withValue("name", "Your Square")
.withValue("type", "square")
.withValue("noOfSides", 4)
.withValue("isPolygon", true),
new QRecord()
.withTableName(table.getName())
.withValue("name", "Some Circle")
.withValue("type", "circle")
.withValue("noOfSides", null)
.withValue("isPolygon", false)
);
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testCustomizer() throws QException
{
QInstance qInstance = TestUtils.defineInstance();
QTableMetaData table = qInstance.getTable(TestUtils.TABLE_NAME_SHAPE);
QSession session = new QSession();
///////////////////////////////////
// add a customizer to the table //
///////////////////////////////////
table.withCustomizer(TableCustomizers.POST_QUERY_RECORD.getRole(), new QCodeReference(ShapeTestCustomizer.class, QCodeUsage.CUSTOMIZER));
//////////////////
// do an insert //
//////////////////
InsertInput insertInput = new InsertInput(qInstance);
insertInput.setSession(session);
insertInput.setTableName(table.getName());
insertInput.setRecords(getTestRecords(table));
new InsertAction().execute(insertInput);
///////////////////////////////////////////////////////
// do a query - assert that the customizer did stuff //
///////////////////////////////////////////////////////
ShapeTestCustomizer.executionCount = 0;
QueryInput queryInput = new QueryInput(qInstance);
queryInput.setSession(session);
queryInput.setTableName(table.getName());
QueryOutput queryOutput = new QueryAction().execute(queryInput);
assertEquals(3, queryOutput.getRecords().size());
assertEquals(3, ShapeTestCustomizer.executionCount);
assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueInteger("id").equals(1) && r.getValueInteger("tenTimesId").equals(10)));
assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueInteger("id").equals(2) && r.getValueInteger("tenTimesId").equals(20)));
assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueInteger("id").equals(3) && r.getValueInteger("tenTimesId").equals(30)));
}
/*******************************************************************************
**
*******************************************************************************/
public static class ShapeTestCustomizer implements Function<QRecord, QRecord>
{
static int executionCount = 0;
@Override
public QRecord apply(QRecord record)
{
executionCount++;
record.setValue("tenTimesId", record.getValueInteger("id") * 10);
return (record);
}
}
}

View File

@ -22,12 +22,15 @@
package com.kingsrook.qqq.backend.core.utils;
import java.io.Serializable;
import java.util.List;
import com.kingsrook.qqq.backend.core.actions.processes.person.addtopeoplesage.AddAge;
import com.kingsrook.qqq.backend.core.actions.processes.person.addtopeoplesage.GetAgeStatistics;
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.adapters.QInstanceAdapter;
import com.kingsrook.qqq.backend.core.actions.values.QCustomPossibleValueProvider;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
@ -40,6 +43,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeUsage;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValue;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
@ -52,6 +56,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.modules.authentication.MockAuthenticationModule;
import com.kingsrook.qqq.backend.core.modules.authentication.metadata.QAuthenticationMetaData;
import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryBackendModule;
import com.kingsrook.qqq.backend.core.modules.backend.implementations.mock.MockBackendModule;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.basic.BasicETLProcess;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamed.StreamedETLProcess;
@ -65,12 +70,14 @@ import com.kingsrook.qqq.backend.core.processes.implementations.mock.MockBackend
public class TestUtils
{
public static final String DEFAULT_BACKEND_NAME = "default";
public static final String MEMORY_BACKEND_NAME = "memory";
public static final String APP_NAME_GREETINGS = "greetingsApp";
public static final String APP_NAME_PEOPLE = "peopleApp";
public static final String APP_NAME_MISCELLANEOUS = "miscellaneous";
public static final String TABLE_NAME_PERSON = "person";
public static final String TABLE_NAME_SHAPE = "shape";
public static final String PROCESS_NAME_GREET_PEOPLE = "greet";
public static final String PROCESS_NAME_GREET_PEOPLE_INTERACTIVE = "greetInteractive";
@ -78,6 +85,10 @@ public class TestUtils
public static final String TABLE_NAME_PERSON_FILE = "personFile";
public static final String TABLE_NAME_ID_AND_NAME_ONLY = "idAndNameOnly";
public static final String POSSIBLE_VALUE_SOURCE_STATE = "state"; // enum-type
public static final String POSSIBLE_VALUE_SOURCE_SHAPE = "shape"; // table-type
public static final String POSSIBLE_VALUE_SOURCE_CUSTOM = "custom"; // custom-type
/*******************************************************************************
@ -89,12 +100,16 @@ public class TestUtils
QInstance qInstance = new QInstance();
qInstance.setAuthentication(defineAuthentication());
qInstance.addBackend(defineBackend());
qInstance.addBackend(defineMemoryBackend());
qInstance.addTable(defineTablePerson());
qInstance.addTable(definePersonFileTable());
qInstance.addTable(defineTableIdAndNameOnly());
qInstance.addTable(defineTableShape());
qInstance.addPossibleValueSource(defineStatesPossibleValueSource());
qInstance.addPossibleValueSource(defineShapePossibleValueSource());
qInstance.addPossibleValueSource(defineCustomPossibleValueSource());
qInstance.addProcess(defineProcessGreetPeople());
qInstance.addProcess(defineProcessGreetPeopleInteractive());
@ -104,8 +119,6 @@ public class TestUtils
defineApps(qInstance);
System.out.println(new QInstanceAdapter().qInstanceToJson(qInstance));
return (qInstance);
}
@ -139,12 +152,40 @@ public class TestUtils
** Define the "states" possible value source used in standard tests
**
*******************************************************************************/
private static QPossibleValueSource<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));
}
@ -174,6 +215,18 @@ public class TestUtils
/*******************************************************************************
** Define the in-memory backend used in standard tests
*******************************************************************************/
public static QBackendMetaData defineMemoryBackend()
{
return new QBackendMetaData()
.withName(MEMORY_BACKEND_NAME)
.withBackendType(MemoryBackendModule.class);
}
/*******************************************************************************
** Define the 'person' table used in standard tests.
*******************************************************************************/
@ -191,7 +244,32 @@ public class TestUtils
.withField(new QFieldMetaData("lastName", QFieldType.STRING))
.withField(new QFieldMetaData("birthDate", QFieldType.DATE))
.withField(new QFieldMetaData("email", QFieldType.STRING))
.withField(new QFieldMetaData("homeState", QFieldType.STRING).withPossibleValueSourceName("state"));
.withField(new QFieldMetaData("homeStateId", QFieldType.INTEGER).withPossibleValueSourceName(POSSIBLE_VALUE_SOURCE_STATE))
.withField(new QFieldMetaData("favoriteShapeId", QFieldType.INTEGER).withPossibleValueSourceName(POSSIBLE_VALUE_SOURCE_SHAPE))
.withField(new QFieldMetaData("customValue", QFieldType.INTEGER).withPossibleValueSourceName(POSSIBLE_VALUE_SOURCE_CUSTOM))
;
}
/*******************************************************************************
** Define the 'shape' table used in standard tests.
*******************************************************************************/
public static QTableMetaData defineTableShape()
{
return new QTableMetaData()
.withName(TABLE_NAME_SHAPE)
.withBackendName(MEMORY_BACKEND_NAME)
.withPrimaryKeyField("id")
.withRecordLabelFields("name")
.withField(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false))
.withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME).withIsEditable(false))
.withField(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME).withIsEditable(false))
.withField(new QFieldMetaData("name", QFieldType.STRING))
.withField(new QFieldMetaData("type", QFieldType.STRING)) // todo PVS
.withField(new QFieldMetaData("noOfSides", QFieldType.INTEGER))
.withField(new QFieldMetaData("isPolygon", QFieldType.BOOLEAN)) // mmm, should be derived from type, no?
;
}
@ -371,6 +449,25 @@ public class TestUtils
/*******************************************************************************
**
*******************************************************************************/
public static void insertDefaultShapes(QInstance qInstance) throws QException
{
List<QRecord> shapeRecords = List.of(
new QRecord().withTableName(TABLE_NAME_SHAPE).withValue("id", 1).withValue("name", "Triangle"),
new QRecord().withTableName(TABLE_NAME_SHAPE).withValue("id", 2).withValue("name", "Square"),
new QRecord().withTableName(TABLE_NAME_SHAPE).withValue("id", 3).withValue("name", "Circle"));
InsertInput insertInput = new InsertInput(qInstance);
insertInput.setSession(new QSession());
insertInput.setTableName(TABLE_NAME_SHAPE);
insertInput.setRecords(shapeRecords);
new InsertAction().execute(insertInput);
}
/*******************************************************************************
**
*******************************************************************************/
@ -417,4 +514,21 @@ public class TestUtils
""");
}
/*******************************************************************************
**
*******************************************************************************/
public static class CustomPossibleValueSource implements QCustomPossibleValueProvider
{
/*******************************************************************************
**
*******************************************************************************/
@Override
public QPossibleValue<?> getPossibleValue(Serializable idValue)
{
return (new QPossibleValue<>(idValue, "Custom[" + idValue + "]"));
}
}
}

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

@ -31,11 +31,11 @@ import com.kingsrook.qqq.backend.module.filesystem.base.actions.AbstractBaseFile
*******************************************************************************/
public interface FilesystemBackendModuleInterface<FILE>
{
String CUSTOMIZER_FILE_POST_FILE_READ = "postFileRead";
/*******************************************************************************
** For filesystem backends, get the module-specific action base-class, that helps
** with functions like listing and deleting files.
*******************************************************************************/
AbstractBaseFilesystemAction<FILE> getActionBase();
}

View File

@ -31,6 +31,8 @@ import java.util.function.Function;
import com.kingsrook.qqq.backend.core.adapters.CsvToQRecordAdapter;
import com.kingsrook.qqq.backend.core.adapters.JsonToQRecordAdapter;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
@ -40,7 +42,6 @@ import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableBackendDetails;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.module.filesystem.base.FilesystemBackendModuleInterface;
import com.kingsrook.qqq.backend.module.filesystem.base.FilesystemRecordBackendDetailFields;
import com.kingsrook.qqq.backend.module.filesystem.base.model.metadata.AbstractFilesystemBackendMetaData;
import com.kingsrook.qqq.backend.module.filesystem.base.model.metadata.AbstractFilesystemTableBackendDetails;
@ -203,7 +204,13 @@ public abstract class AbstractBaseFilesystemAction<FILE>
if(queryInput.getRecordPipe() != null)
{
new CsvToQRecordAdapter().buildRecordsFromCsv(queryInput.getRecordPipe(), fileContents, table, null, (record -> addBackendDetailsToRecord(record, file)));
new CsvToQRecordAdapter().buildRecordsFromCsv(queryInput.getRecordPipe(), fileContents, table, null, (record ->
{
////////////////////////////////////////////////////////////////////////////////////////////
// Before the records go into the pipe, make sure their backend details are added to them //
////////////////////////////////////////////////////////////////////////////////////////////
addBackendDetailsToRecord(record, file);
}));
}
else
{
@ -243,6 +250,24 @@ public abstract class AbstractBaseFilesystemAction<FILE>
/*******************************************************************************
**
*******************************************************************************/
public CountOutput executeCount(CountInput countInput) throws QException
{
QueryInput queryInput = new QueryInput(countInput.getInstance());
queryInput.setSession(countInput.getSession());
queryInput.setTableName(countInput.getTableName());
queryInput.setFilter(countInput.getFilter());
QueryOutput queryOutput = executeQuery(queryInput);
CountOutput countOutput = new CountOutput();
countOutput.setCount(queryOutput.getRecords().size());
return (countOutput);
}
/*******************************************************************************
** Add backend details to records about the file that they are in.
*******************************************************************************/
@ -281,7 +306,7 @@ public abstract class AbstractBaseFilesystemAction<FILE>
*******************************************************************************/
private String customizeFileContentsAfterReading(QTableMetaData table, String fileContents) throws QException
{
Optional<QCodeReference> optionalCustomizer = table.getCustomizer(FilesystemBackendModuleInterface.CUSTOMIZER_FILE_POST_FILE_READ);
Optional<QCodeReference> optionalCustomizer = table.getCustomizer(FilesystemTableCustomizers.POST_READ_FILE.getRole());
if(optionalCustomizer.isEmpty())
{
return (fileContents);

View File

@ -0,0 +1,72 @@
package com.kingsrook.qqq.backend.module.filesystem.base.actions;
import java.util.function.Function;
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizer;
/*******************************************************************************
**
*******************************************************************************/
public enum FilesystemTableCustomizers
{
POST_READ_FILE(new TableCustomizer("postReadFile", Function.class, ((Object x) ->
{
Function<String, String> function = (Function<String, String>) x;
String output = function.apply(new String());
})));
private final TableCustomizer tableCustomizer;
/*******************************************************************************
**
*******************************************************************************/
FilesystemTableCustomizers(TableCustomizer tableCustomizer)
{
this.tableCustomizer = tableCustomizer;
}
/*******************************************************************************
** Get the FilesystemTableCustomer for a given role (e.g., the role used in meta-data, not
** the enum-constant name).
*******************************************************************************/
public static FilesystemTableCustomizers forRole(String name)
{
for(FilesystemTableCustomizers value : values())
{
if(value.tableCustomizer.getRole().equals(name))
{
return (value);
}
}
return (null);
}
/*******************************************************************************
** Getter for tableCustomizer
**
*******************************************************************************/
public TableCustomizer getTableCustomizer()
{
return tableCustomizer;
}
/*******************************************************************************
** get the role from the tableCustomizer
**
*******************************************************************************/
public String getRole()
{
return (tableCustomizer.getRole());
}
}

View File

@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.module.filesystem.local;
import java.io.File;
import com.kingsrook.qqq.backend.core.actions.interfaces.CountInterface;
import com.kingsrook.qqq.backend.core.actions.interfaces.DeleteInterface;
import com.kingsrook.qqq.backend.core.actions.interfaces.InsertInterface;
import com.kingsrook.qqq.backend.core.actions.interfaces.QueryInterface;
@ -33,6 +34,7 @@ import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
import com.kingsrook.qqq.backend.module.filesystem.base.FilesystemBackendModuleInterface;
import com.kingsrook.qqq.backend.module.filesystem.base.actions.AbstractBaseFilesystemAction;
import com.kingsrook.qqq.backend.module.filesystem.local.actions.AbstractFilesystemAction;
import com.kingsrook.qqq.backend.module.filesystem.local.actions.FilesystemCountAction;
import com.kingsrook.qqq.backend.module.filesystem.local.actions.FilesystemDeleteAction;
import com.kingsrook.qqq.backend.module.filesystem.local.actions.FilesystemInsertAction;
import com.kingsrook.qqq.backend.module.filesystem.local.actions.FilesystemQueryAction;
@ -107,6 +109,16 @@ public class FilesystemBackendModule implements QBackendModuleInterface, Filesys
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public CountInterface getCountInterface()
{
return new FilesystemCountAction();
}
/*******************************************************************************
**

View File

@ -0,0 +1,45 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.module.filesystem.local.actions;
import com.kingsrook.qqq.backend.core.actions.interfaces.CountInterface;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput;
/*******************************************************************************
**
*******************************************************************************/
public class FilesystemCountAction extends AbstractFilesystemAction implements CountInterface
{
/*******************************************************************************
**
*******************************************************************************/
public CountOutput execute(CountInput countInput) throws QException
{
return (executeCount(countInput));
}
}

View File

@ -0,0 +1,45 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.module.filesystem.s3.actions;
import com.kingsrook.qqq.backend.core.actions.interfaces.CountInterface;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput;
/*******************************************************************************
**
*******************************************************************************/
public class S3CountAction extends AbstractS3Action implements CountInterface
{
/*******************************************************************************
**
*******************************************************************************/
public CountOutput execute(CountInput countInput) throws QException
{
return (executeCount(countInput));
}
}

View File

@ -73,11 +73,14 @@ public class FilesystemActionTest
TestUtils.increaseTestInstanceCounter();
FilesystemBackendMetaData filesystemBackendMetaData = TestUtils.defineLocalFilesystemBackend();
File baseDirectory = new File(filesystemBackendMetaData.getBasePath());
boolean mkdirsResult = baseDirectory.mkdirs();
if(!mkdirsResult)
File baseDirectory = new File(filesystemBackendMetaData.getBasePath());
if(!baseDirectory.exists())
{
fail("Failed to make directories at [" + baseDirectory + "] for filesystem backend module");
boolean mkdirsResult = baseDirectory.mkdirs();
if(!mkdirsResult)
{
fail("Failed to make directories at [" + baseDirectory + "] for filesystem backend module");
}
}
writePersonJSONFiles(baseDirectory);
@ -92,9 +95,9 @@ public class FilesystemActionTest
private void writePersonJSONFiles(File baseDirectory) throws IOException
{
String fullPath = baseDirectory.getAbsolutePath();
if (TestUtils.defineLocalFilesystemJSONPersonTable().getBackendDetails() instanceof FilesystemTableBackendDetails details)
if(TestUtils.defineLocalFilesystemJSONPersonTable().getBackendDetails() instanceof FilesystemTableBackendDetails details)
{
if (StringUtils.hasContent(details.getBasePath()))
if(StringUtils.hasContent(details.getBasePath()))
{
fullPath += File.separatorChar + details.getBasePath();
}
@ -125,9 +128,9 @@ public class FilesystemActionTest
private void writePersonCSVFiles(File baseDirectory) throws IOException
{
String fullPath = baseDirectory.getAbsolutePath();
if (TestUtils.defineLocalFilesystemCSVPersonTable().getBackendDetails() instanceof FilesystemTableBackendDetails details)
if(TestUtils.defineLocalFilesystemCSVPersonTable().getBackendDetails() instanceof FilesystemTableBackendDetails details)
{
if (StringUtils.hasContent(details.getBasePath()))
if(StringUtils.hasContent(details.getBasePath()))
{
fullPath += File.separatorChar + details.getBasePath();
}

View File

@ -0,0 +1,52 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.module.filesystem.local.actions;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput;
import com.kingsrook.qqq.backend.module.filesystem.TestUtils;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
/*******************************************************************************
** Unit test for FilesystemCountAction
*******************************************************************************/
public class FilesystemCountActionTest extends FilesystemActionTest
{
/*******************************************************************************
**
*******************************************************************************/
@Test
public void testCount1() throws QException
{
CountInput countInput = new CountInput();
countInput.setInstance(TestUtils.defineInstance());
countInput.setTableName(TestUtils.defineLocalFilesystemJSONPersonTable().getName());
CountOutput countOutput = new FilesystemCountAction().execute(countInput);
Assertions.assertEquals(3, countOutput.getCount(), "Unfiltered count should find all rows");
}
}

View File

@ -0,0 +1,47 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.module.filesystem.local.actions;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
import org.apache.commons.lang.NotImplementedException;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertThrows;
/*******************************************************************************
**
*******************************************************************************/
public class FilesystemDeleteActionTest extends FilesystemActionTest
{
/*******************************************************************************
**
*******************************************************************************/
@Test
public void test() throws QException
{
assertThrows(NotImplementedException.class, () -> new FilesystemDeleteAction().execute(new DeleteInput()));
}
}

View File

@ -0,0 +1,47 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.module.filesystem.local.actions;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import org.apache.commons.lang.NotImplementedException;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertThrows;
/*******************************************************************************
**
*******************************************************************************/
public class FilesystemInsertActionTest extends FilesystemActionTest
{
/*******************************************************************************
**
*******************************************************************************/
@Test
public void test() throws QException
{
assertThrows(NotImplementedException.class, () -> new FilesystemInsertAction().execute(new InsertInput()));
}
}

View File

@ -26,14 +26,13 @@ import java.util.function.Function;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeUsage;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeUsage;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.module.filesystem.TestUtils;
import com.kingsrook.qqq.backend.module.filesystem.base.FilesystemBackendModuleInterface;
import com.kingsrook.qqq.backend.module.filesystem.base.FilesystemRecordBackendDetailFields;
import com.kingsrook.qqq.backend.module.filesystem.base.actions.FilesystemTableCustomizers;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
@ -72,10 +71,7 @@ public class FilesystemQueryActionTest extends FilesystemActionTest
QInstance instance = TestUtils.defineInstance();
QTableMetaData table = instance.getTable(TestUtils.TABLE_NAME_PERSON_LOCAL_FS_JSON);
table.withCustomizer(FilesystemBackendModuleInterface.CUSTOMIZER_FILE_POST_FILE_READ, new QCodeReference()
.withName(ValueUpshifter.class.getName())
.withCodeType(QCodeType.JAVA)
.withCodeUsage(QCodeUsage.CUSTOMIZER));
table.withCustomizer(FilesystemTableCustomizers.POST_READ_FILE.getRole(), new QCodeReference(ValueUpshifter.class, QCodeUsage.CUSTOMIZER));
queryInput.setInstance(instance);
queryInput.setTableName(TestUtils.defineLocalFilesystemJSONPersonTable().getName());

View File

@ -0,0 +1,47 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.module.filesystem.local.actions;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
import org.apache.commons.lang.NotImplementedException;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertThrows;
/*******************************************************************************
**
*******************************************************************************/
public class FilesystemUpdateActionTest extends FilesystemActionTest
{
/*******************************************************************************
**
*******************************************************************************/
@Test
public void test() throws QException
{
assertThrows(NotImplementedException.class, () -> new FilesystemUpdateAction().execute(new UpdateInput()));
}
}

View File

@ -31,8 +31,6 @@ import com.amazonaws.services.s3.model.S3ObjectSummary;
import com.kingsrook.qqq.backend.module.filesystem.s3.utils.S3Utils;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.condition.DisabledOnOs;
import org.junit.jupiter.api.condition.OS;
import org.junit.jupiter.api.extension.ExtendWith;
@ -40,7 +38,7 @@ import org.junit.jupiter.api.extension.ExtendWith;
** Base class for tests that want to be able to work with localstack s3.
*******************************************************************************/
@ExtendWith(LocalstackDockerExtension.class)
@LocalstackDockerProperties(services = { ServiceName.S3 }, portEdge = "2960", portElasticSearch = "2961")
@LocalstackDockerProperties(useSingleDockerContainer = true, services = { ServiceName.S3 }, portEdge = "2960", portElasticSearch = "2961")
public class BaseS3Test
{
public static final String BUCKET_NAME = "localstack-test-bucket";

View File

@ -0,0 +1,66 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.module.filesystem.s3.actions;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput;
import com.kingsrook.qqq.backend.module.filesystem.TestUtils;
import com.kingsrook.qqq.backend.module.filesystem.s3.BaseS3Test;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
/*******************************************************************************
**
*******************************************************************************/
public class S3CountActionTest extends BaseS3Test
{
/*******************************************************************************
**
*******************************************************************************/
@Test
public void testCount1() throws QException
{
CountInput countInput = initCountRequest();
S3CountAction s3CountAction = new S3CountAction();
s3CountAction.setS3Utils(getS3Utils());
CountOutput countOutput = s3CountAction.execute(countInput);
Assertions.assertEquals(5, countOutput.getCount(), "Expected # of rows from unfiltered count");
}
/*******************************************************************************
**
*******************************************************************************/
private CountInput initCountRequest() throws QException
{
CountInput countInput = new CountInput();
countInput.setInstance(TestUtils.defineInstance());
countInput.setTableName(TestUtils.defineS3CSVPersonTable().getName());
return countInput;
}
}

View File

@ -0,0 +1,48 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.module.filesystem.s3.actions;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
import com.kingsrook.qqq.backend.module.filesystem.s3.BaseS3Test;
import org.apache.commons.lang.NotImplementedException;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertThrows;
/*******************************************************************************
**
*******************************************************************************/
public class S3DeleteActionTest extends BaseS3Test
{
/*******************************************************************************
**
*******************************************************************************/
@Test
public void test() throws QException
{
assertThrows(NotImplementedException.class, () -> new S3DeleteAction().execute(new DeleteInput()));
}
}

View File

@ -0,0 +1,48 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.module.filesystem.s3.actions;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.module.filesystem.s3.BaseS3Test;
import org.apache.commons.lang.NotImplementedException;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertThrows;
/*******************************************************************************
**
*******************************************************************************/
public class S3InsertActionTest extends BaseS3Test
{
/*******************************************************************************
**
*******************************************************************************/
@Test
public void test() throws QException
{
assertThrows(NotImplementedException.class, () -> new S3InsertAction().execute(new InsertInput()));
}
}

View File

@ -0,0 +1,48 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.module.filesystem.s3.actions;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
import com.kingsrook.qqq.backend.module.filesystem.s3.BaseS3Test;
import org.apache.commons.lang.NotImplementedException;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertThrows;
/*******************************************************************************
**
*******************************************************************************/
public class S3UpdateActionTest extends BaseS3Test
{
/*******************************************************************************
**
*******************************************************************************/
@Test
public void test() throws QException
{
assertThrows(NotImplementedException.class, () -> new S3UpdateAction().execute(new UpdateInput()));
}
}

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

@ -29,6 +29,7 @@ import java.sql.Connection;
import java.sql.Date;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Timestamp;
@ -277,8 +278,6 @@ public class QueryManager
*******************************************************************************/
public static SimpleEntity executeStatementForSimpleEntity(Connection connection, String sql, Object... params) throws SQLException
{
throw (new NotImplementedException());
/*
PreparedStatement statement = prepareStatementAndBindParams(connection, sql, params);
statement.execute();
ResultSet resultSet = statement.getResultSet();
@ -290,7 +289,6 @@ public class QueryManager
{
return (null);
}
*/
}
@ -355,8 +353,6 @@ public class QueryManager
*******************************************************************************/
public static SimpleEntity buildSimpleEntity(ResultSet resultSet) throws SQLException
{
throw (new NotImplementedException());
/*
SimpleEntity row = new SimpleEntity();
ResultSetMetaData metaData = resultSet.getMetaData();
@ -365,7 +361,6 @@ public class QueryManager
row.put(metaData.getColumnName(i), getObject(resultSet, i));
}
return row;
*/
}

View File

@ -37,7 +37,7 @@ import com.kingsrook.qqq.backend.module.rdbms.jdbc.QueryManager;
import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaData;
import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSTableBackendDetails;
import org.apache.commons.io.IOUtils;
import static junit.framework.Assert.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNotNull;
/*******************************************************************************

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

@ -360,4 +360,25 @@ class QueryManagerTest
assertEquals(null, QueryManager.executeStatementForSingleValue(connection, Integer.class, "SELECT int_col FROM test_table WHERE int_col IS NULL"));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testQueryForSimpleEntity() throws SQLException
{
Connection connection = getConnection();
QueryManager.executeUpdate(connection, """
INSERT INTO test_table
( int_col, datetime_col, char_col, date_col, time_col )
VALUES
( 47, '2022-08-10 19:22:08', 'Q', '2022-08-10', '19:22:08')
""");
SimpleEntity simpleEntity = QueryManager.executeStatementForSimpleEntity(connection, "SELECT * FROM test_table");
assertNotNull(simpleEntity);
assertEquals(47, simpleEntity.get("INT_COL"));
assertEquals("Q", simpleEntity.get("CHAR_COL"));
}
}

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;
@ -109,11 +110,16 @@ public class QJavalinImplementation
{
private static final Logger LOG = LogManager.getLogger(QJavalinImplementation.class);
private static final int SESSION_COOKIE_AGE = 60 * 60 * 24;
private static final int SESSION_COOKIE_AGE = 60 * 60 * 24;
private static final String SESSION_ID_COOKIE_NAME = "sessionId";
static QInstance qInstance;
private static Supplier<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,50 @@ public class QJavalinImplementation
// todo base path from arg? - and then potentially multiple instances too (chosen based on the root path??)
service = Javalin.create().start(port);
service.routes(getRoutes());
service.before(QJavalinImplementation::hotSwapQInstance);
}
/*******************************************************************************
** If there's a qInstanceHotSwapSupplier, and its been a little while, replace
** the qInstance with a new one from the supplier. Meant to be used while doing
** development.
*******************************************************************************/
public static void hotSwapQInstance(Context context)
{
if(qInstanceHotSwapSupplier != null)
{
long now = System.currentTimeMillis();
if(now - lastQInstanceHotSwapMillis < MILLIS_BETWEEN_HOT_SWAPS)
{
return;
}
lastQInstanceHotSwapMillis = now;
try
{
QInstance newQInstance = qInstanceHotSwapSupplier.get();
if(newQInstance == null)
{
LOG.warn("Got a null qInstance from hotSwapSupplier. Not hot-swapping.");
return;
}
new QInstanceValidator().validate(newQInstance);
QJavalinImplementation.qInstance = newQInstance;
LOG.info("Swapped qInstance");
}
catch(QInstanceValidationException e)
{
LOG.warn(e.getMessage());
}
catch(Exception e)
{
LOG.error("Error swapping QInstance", e);
}
}
}
@ -249,7 +299,7 @@ public class QJavalinImplementation
static void setupSession(Context context, AbstractActionInput input) throws QModuleDispatchException
{
QAuthenticationModuleDispatcher qAuthenticationModuleDispatcher = new QAuthenticationModuleDispatcher();
QAuthenticationModuleInterface authenticationModule = qAuthenticationModuleDispatcher.getQModule(input.getAuthenticationMetaData());
QAuthenticationModuleInterface authenticationModule = qAuthenticationModuleDispatcher.getQModule(input.getAuthenticationMetaData());
try
{
@ -266,7 +316,7 @@ public class QJavalinImplementation
else
{
String authorizationHeaderValue = context.header("Authorization");
if (authorizationHeaderValue != null)
if(authorizationHeaderValue != null)
{
String bearerPrefix = "Bearer ";
if(authorizationHeaderValue.startsWith(bearerPrefix))
@ -309,7 +359,7 @@ public class QJavalinImplementation
{
try
{
String table = context.pathParam("table");
String table = context.pathParam("table");
List<Serializable> primaryKeys = new ArrayList<>();
primaryKeys.add(context.pathParam("primaryKey"));
@ -338,9 +388,9 @@ public class QJavalinImplementation
{
try
{
String table = context.pathParam("table");
String table = context.pathParam("table");
List<QRecord> recordList = new ArrayList<>();
QRecord record = new QRecord();
QRecord record = new QRecord();
record.setTableName(table);
recordList.add(record);
@ -382,9 +432,9 @@ public class QJavalinImplementation
{
try
{
String table = context.pathParam("table");
String table = context.pathParam("table");
List<QRecord> recordList = new ArrayList<>();
QRecord record = new QRecord();
QRecord record = new QRecord();
record.setTableName(table);
recordList.add(record);
@ -429,6 +479,8 @@ public class QJavalinImplementation
setupSession(context, queryInput);
queryInput.setTableName(tableName);
queryInput.setShouldGenerateDisplayValues(true);
queryInput.setShouldTranslatePossibleValues(true);
// todo - validate that the primary key is of the proper type (e.g,. not a string for an id field)
// and throw a 400-series error (tell the user bad-request), rather than, we're doing a 500 (server error)
@ -524,6 +576,8 @@ public class QJavalinImplementation
QueryInput queryInput = new QueryInput(qInstance);
setupSession(context, queryInput);
queryInput.setTableName(context.pathParam("table"));
queryInput.setShouldGenerateDisplayValues(true);
queryInput.setShouldTranslatePossibleValues(true);
queryInput.setSkip(integerQueryParam(context, "skip"));
queryInput.setLimit(integerQueryParam(context, "limit"));
@ -836,4 +890,14 @@ public class QJavalinImplementation
return (null);
}
/*******************************************************************************
** Setter for qInstanceHotSwapSupplier
*******************************************************************************/
public static void setQInstanceHotSwapSupplier(Supplier<QInstance> qInstanceHotSwapSupplier)
{
QJavalinImplementation.qInstanceHotSwapSupplier = qInstanceHotSwapSupplier;
}
}

View File

@ -50,7 +50,7 @@ import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager;
import com.kingsrook.qqq.backend.module.rdbms.jdbc.QueryManager;
import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaData;
import org.apache.commons.io.IOUtils;
import static junit.framework.Assert.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNotNull;
/*******************************************************************************

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

@ -44,7 +44,7 @@ import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager;
import com.kingsrook.qqq.backend.module.rdbms.jdbc.QueryManager;
import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaData;
import org.apache.commons.io.IOUtils;
import static junit.framework.Assert.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNotNull;
/*******************************************************************************

View File

@ -98,6 +98,11 @@
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-core</artifactId>
<version>4.10.0</version>
</dependency>
</dependencies>
<build>
@ -116,6 +121,19 @@
</archive>
</configuration>
</plugin>
<plugin>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-maven-plugin</artifactId>
<version>4.10.0</version>
<configuration>
<propertyFile>/src/main/resources/liquibase/liquibase.properties</propertyFile>
<url>${env.LB_DB_URL}</url>
<username>${env.LB_DB_USERNAME}</username>
<password>${env.LB_DB_PASSWORD}</password>
<contexts>${env.LB_CONTEXTS}</contexts>
</configuration>
</plugin>
</plugins>
</build>

View File

@ -36,7 +36,8 @@ public class SampleCli
*******************************************************************************/
public static void main(String[] args)
{
new SampleCli().run(args);
int exitCode = new SampleCli().run(args);
System.exit(exitCode);
}
@ -44,19 +45,19 @@ public class SampleCli
/*******************************************************************************
**
*******************************************************************************/
private void run(String[] args)
int run(String[] args)
{
try
{
QInstance qInstance = SampleMetaDataProvider.defineInstance();
QPicoCliImplementation qPicoCliImplementation = new QPicoCliImplementation(qInstance);
int exitCode = qPicoCliImplementation.runCli("my-sample-cli", args);
System.exit(exitCode);
return (qPicoCliImplementation.runCli("my-sample-cli", args));
}
catch(Exception e)
{
e.printStackTrace();
System.exit(-1);
return (-1);
}
}

View File

@ -22,7 +22,6 @@
package com.kingsrook.sampleapp;
import com.kingsrook.qqq.backend.core.instances.QInstanceValidator;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.javalin.QJavalinImplementation;
import io.javalin.Javalin;
@ -71,6 +70,24 @@ public class SampleJavalinServer
config.enableCorsForAllOrigins();
}).start(PORT);
javalinService.routes(qJavalinImplementation.getRoutes());
/////////////////////////////////////////////////////////////////
// set the server to hot-swap the q instance before all routes //
/////////////////////////////////////////////////////////////////
QJavalinImplementation.setQInstanceHotSwapSupplier(() ->
{
try
{
return (SampleMetaDataProvider.defineInstance());
}
catch(Exception e)
{
LOG.warn("Error hot-swapping meta data", e);
return (null);
}
});
javalinService.before(QJavalinImplementation::hotSwapQInstance);
javalinService.after(ctx ->
ctx.res.setHeader("Access-Control-Allow-Origin", "http://localhost:3000"));
}

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))
@ -261,12 +261,13 @@ public class SampleMetaDataProvider
.withField(new QFieldMetaData("lastName", QFieldType.STRING).withBackendName("last_name").withIsRequired(true))
.withField(new QFieldMetaData("birthDate", QFieldType.DATE).withBackendName("birth_date"))
.withField(new QFieldMetaData("email", QFieldType.STRING))
.withField(new QFieldMetaData("isEmployed", QFieldType.BOOLEAN).withBackendName("is_employed"))
.withField(new QFieldMetaData("annualSalary", QFieldType.DECIMAL).withBackendName("annual_salary").withDisplayFormat(DisplayFormat.CURRENCY))
.withField(new QFieldMetaData("daysWorked", QFieldType.INTEGER).withBackendName("days_worked").withDisplayFormat(DisplayFormat.COMMAS))
.withSection(new QFieldSection("identity", "Identity", new QIcon("badge"), Tier.T1, List.of("id", "firstName", "lastName")))
.withSection(new QFieldSection("basicInfo", "Basic Info", new QIcon("dataset"), Tier.T2, List.of("email", "birthDate")))
.withSection(new QFieldSection("employmentInfo", "Employment Info", new QIcon("work"), Tier.T2, List.of("annualSalary", "daysWorked")))
.withSection(new QFieldSection("employmentInfo", "Employment Info", new QIcon("work"), Tier.T2, List.of("isEmployed", "annualSalary", "daysWorked")))
.withSection(new QFieldSection("dates", "Dates", new QIcon("calendar_month"), Tier.T3, List.of("createDate", "modifyDate")));
QInstanceEnricher.setInferredFieldBackendNames(qTableMetaData);

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:pro="http://www.liquibase.org/xml/ns/pro"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd
http://www.liquibase.org/xml/ns/pro
http://www.liquibase.org/xml/ns/pro/liquibase-pro-4.1.xsd">
<include file="changesets/initial.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>

View File

@ -0,0 +1,100 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:pro="http://www.liquibase.org/xml/ns/pro"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd
http://www.liquibase.org/xml/ns/pro
http://www.liquibase.org/xml/ns/pro/liquibase-pro-4.1.xsd">
<changeSet author="tchamberlain" id="initial-1">
<sql>
DROP TABLE IF EXISTS person;
CREATE TABLE person
(
id INT AUTO_INCREMENT primary key ,
create_date TIMESTAMP DEFAULT now(),
modify_date TIMESTAMP DEFAULT now(),
first_name VARCHAR(80) NOT NULL,
last_name VARCHAR(80) NOT NULL,
birth_date DATE,
email VARCHAR(250) NOT NULL,
is_employed BOOLEAN,
annual_salary DECIMAL(12,2),
days_worked INTEGER
);
INSERT INTO person (id, first_name, last_name, birth_date, email, is_employed, annual_salary, days_worked) VALUES (1, 'Darin', 'Kelkhoff', '1980-05-31', 'darin.kelkhoff@gmail.com', 1, 25000, 27);
INSERT INTO person (id, first_name, last_name, birth_date, email, is_employed, annual_salary, days_worked) VALUES (2, 'James', 'Maes', '1980-05-15', 'jmaes@mmltholdings.com', 1, 26000, 124);
INSERT INTO person (id, first_name, last_name, birth_date, email, is_employed, annual_salary, days_worked) VALUES (3, 'Tim', 'Chamberlain', '1976-05-28', 'tchamberlain@mmltholdings.com', 0, null, 0);
INSERT INTO person (id, first_name, last_name, birth_date, email, is_employed, annual_salary, days_worked) VALUES (4, 'Tyler', 'Samples', NULL, 'tsamples@mmltholdings.com', 1, 30000, 99);
INSERT INTO person (id, first_name, last_name, birth_date, email, is_employed, annual_salary, days_worked) VALUES (5, 'Garret', 'Richardson', '1981-01-01', 'grichardson@mmltholdings.com', 1, 1000000, 232);
DROP TABLE IF EXISTS carrier;
CREATE TABLE carrier
(
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(80) NOT NULL,
company_code VARCHAR(80) NOT NULL,
service_level VARCHAR(80) NOT NULL
);
INSERT INTO carrier (id, name, company_code, service_level) VALUES (1, 'UPS Ground', 'UPS', 'G');
INSERT INTO carrier (id, name, company_code, service_level) VALUES (2, 'UPS 2Day', 'UPS', '2');
INSERT INTO carrier (id, name, company_code, service_level) VALUES (3, 'UPS International', 'UPS', 'I');
INSERT INTO carrier (id, name, company_code, service_level) VALUES (4, 'Fedex Ground', 'FEDEX', 'G');
INSERT INTO carrier (id, name, company_code, service_level) VALUES (5, 'Fedex Next Day', 'UPS', '1');
INSERT INTO carrier (id, name, company_code, service_level) VALUES (6, 'Will Call', 'WILL_CALL', 'W');
INSERT INTO carrier (id, name, company_code, service_level) VALUES (7, 'USPS Priority', 'USPS', '1');
INSERT INTO carrier (id, name, company_code, service_level) VALUES (8, 'USPS Super Slow', 'USPS', '4');
INSERT INTO carrier (id, name, company_code, service_level) VALUES (9, 'USPS Super Fast', 'USPS', '0');
INSERT INTO carrier (id, name, company_code, service_level) VALUES (10, 'DHL International', 'DHL', 'I');
INSERT INTO carrier (id, name, company_code, service_level) VALUES (11, 'GSO', 'GSO', 'G');
DROP TABLE IF EXISTS child_table;
CREATE TABLE child_table
(
id INT AUTO_INCREMENT primary key,
name VARCHAR(80) NOT NULL
);
INSERT INTO child_table (id, name) VALUES (1, 'Timmy');
INSERT INTO child_table (id, name) VALUES (2, 'Jimmy');
INSERT INTO child_table (id, name) VALUES (3, 'Johnny');
INSERT INTO child_table (id, name) VALUES (4, 'Gracie');
INSERT INTO child_table (id, name) VALUES (5, 'Suzie');
DROP TABLE IF EXISTS parent_table;
CREATE TABLE parent_table
(
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(80) NOT NULL,
child_id INT,
foreign key (child_id) references child_table(id)
);
INSERT INTO parent_table (id, name, child_id) VALUES (1, 'Tim''s Dad', 1);
INSERT INTO parent_table (id, name, child_id) VALUES (2, 'Tim''s Mom', 1);
INSERT INTO parent_table (id, name, child_id) VALUES (3, 'Childless Man', null);
INSERT INTO parent_table (id, name, child_id) VALUES (4, 'Childless Woman', null);
INSERT INTO parent_table (id, name, child_id) VALUES (5, 'Johny''s Single Dad', 3);
DROP TABLE IF EXISTS city;
CREATE TABLE city
(
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(80) NOT NULL,
state VARCHAR(2) NOT NULL
);
INSERT INTO city (id, name, state) VALUES (1, 'Decatur', 'IL');
INSERT INTO city (id, name, state) VALUES (2, 'Chester', 'IL');
INSERT INTO city (id, name, state) VALUES (3, 'St. Louis', 'MO');
INSERT INTO city (id, name, state) VALUES (4, 'Baltimore', 'MD');
INSERT INTO city (id, name, state) VALUES (5, 'New York', 'NY');
</sql>
</changeSet>
</databaseChangeLog>

View File

@ -0,0 +1,6 @@
#liquibase.properties
classpath: /src/main/resources/liquibase/lib/mysql-connector-java-8.0.29.jar
driver: com.mysql.cj.jdbc.Driver
changeLogFile:/src/main/resources/liquibase/changelog.xml
logLevel: INFO
liquibase.hub.mode=off

View File

@ -0,0 +1,58 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.sampleapp;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
/*******************************************************************************
** Unit test for SampleCli
*******************************************************************************/
class SampleCliTest
{
/*******************************************************************************
**
*******************************************************************************/
@Test
void testExitSuccess() throws QException
{
int exitCode = new SampleCli().run(new String[] { "--meta-data" });
assertEquals(0, exitCode);
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testNotExitSuccess() throws QException
{
int exitCode = new SampleCli().run(new String[] { "asdfasdf" });
assertNotEquals(0, exitCode);
}
}

View File

@ -2,14 +2,14 @@ package com.kingsrook.sampleapp;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
/*******************************************************************************
** Unit test for com.kingsrook.sampleapp.SampleJavalinServer
** Unit test for SampleJavalinServer
*******************************************************************************/
class SampleJavalinServerTest
{
/*******************************************************************************
**
*******************************************************************************/

View File

@ -43,7 +43,6 @@ import com.kingsrook.qqq.backend.module.filesystem.local.actions.FilesystemQuery
import com.kingsrook.qqq.backend.module.filesystem.local.model.metadata.FilesystemTableBackendDetails;
import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager;
import com.kingsrook.qqq.backend.module.rdbms.jdbc.QueryManager;
import junit.framework.Assert;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.junit.jupiter.api.Assertions;
@ -84,7 +83,7 @@ class SampleMetaDataProviderTest
try(Connection connection = connectionManager.getConnection(SampleMetaDataProvider.defineRdbmsBackend()))
{
InputStream primeTestDatabaseSqlStream = SampleMetaDataProviderTest.class.getResourceAsStream("/" + sqlFileName);
Assert.assertNotNull(primeTestDatabaseSqlStream);
assertNotNull(primeTestDatabaseSqlStream);
List<String> lines = (List<String>) IOUtils.readLines(primeTestDatabaseSqlStream);
lines = lines.stream().filter(line -> !line.startsWith("-- ")).toList();
String joinedSQL = String.join("\n", lines);

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