diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/processes/RunBackendStepAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/processes/RunBackendStepAction.java index 7daf8de9..1d242057 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/processes/RunBackendStepAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/processes/RunBackendStepAction.java @@ -215,7 +215,7 @@ public class RunBackendStepAction Object codeObject = codeClass.getConstructor().newInstance(); if(!(codeObject instanceof BackendStep backendStepCodeObject)) { - throw (new QException("The supplied code [" + codeClass.getName() + "] is not an instance of FunctionBody")); + throw (new QException("The supplied code [" + codeClass.getName() + "] is not an instance of BackendStep")); } backendStepCodeObject.run(runBackendStepInput, runBackendStepOutput); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/QueryAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/QueryAction.java index eec134f0..83ec1dd1 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/QueryAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/QueryAction.java @@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.actions.tables; import com.kingsrook.qqq.backend.core.actions.ActionHelper; +import com.kingsrook.qqq.backend.core.actions.values.QPossibleValueTranslator; import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput; @@ -45,14 +46,24 @@ public class QueryAction ActionHelper.validateSession(queryInput); QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher(); - QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(queryInput.getBackend()); + QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(queryInput.getBackend()); // todo pre-customization - just get to modify the request? QueryOutput queryOutput = qModule.getQueryInterface().execute(queryInput); // todo post-customization - can do whatever w/ the result if you want - if (queryInput.getRecordPipe() == null) + if(queryInput.getRecordPipe() == null) { - QValueFormatter.setDisplayValuesInRecords(queryInput.getTable(), queryOutput.getRecords()); + if(queryInput.getShouldGenerateDisplayValues()) + { + QValueFormatter qValueFormatter = new QValueFormatter(); + qValueFormatter.setDisplayValuesInRecords(queryInput.getTable(), queryOutput.getRecords()); + } + + if(queryInput.getShouldTranslatePossibleValues()) + { + QPossibleValueTranslator qPossibleValueTranslator = new QPossibleValueTranslator(queryInput.getInstance(), queryInput.getSession()); + qPossibleValueTranslator.translatePossibleValuesInRecords(queryInput.getTable(), queryOutput.getRecords()); + } } return queryOutput; diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QCustomPossibleValueProvider.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QCustomPossibleValueProvider.java new file mode 100644 index 00000000..5f2cdc17 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QCustomPossibleValueProvider.java @@ -0,0 +1,43 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.actions.values; + + +import java.io.Serializable; +import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValue; + + +/******************************************************************************* + ** Interface to be implemented by user-defined code that serves as the backing + ** for a CUSTOM type possibleValueSource + *******************************************************************************/ +public interface QCustomPossibleValueProvider +{ + + /******************************************************************************* + ** + *******************************************************************************/ + QPossibleValue getPossibleValue(Serializable idValue); + + // todo - get/search list of possible values + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QPossibleValueTranslator.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QPossibleValueTranslator.java new file mode 100644 index 00000000..603951ee --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QPossibleValueTranslator.java @@ -0,0 +1,359 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.actions.values; + + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput; +import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValue; +import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource; +import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.model.session.QSession; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import com.kingsrook.qqq.backend.core.utils.ListingHash; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + + +/******************************************************************************* + ** Class responsible for looking up possible-values for fields/records and + ** make them into display values. + *******************************************************************************/ +public class QPossibleValueTranslator +{ + private static final Logger LOG = LogManager.getLogger(QPossibleValueTranslator.class); + + private final QInstance qInstance; + private final QSession session; + + // top-level keys are pvsNames (not table names) + // 2nd-level keys are pkey values from the PVS table + private Map> possibleValueCache; + + + + /******************************************************************************* + ** + *******************************************************************************/ + public QPossibleValueTranslator(QInstance qInstance, QSession session) + { + this.qInstance = qInstance; + this.session = session; + + this.possibleValueCache = new HashMap<>(); + } + + + + /******************************************************************************* + ** For a list of records, translate their possible values (populating their display values) + *******************************************************************************/ + public void translatePossibleValuesInRecords(QTableMetaData table, List records) + { + if(records == null) + { + return; + } + + primePvsCache(table, records); + + for(QRecord record : records) + { + for(QFieldMetaData field : table.getFields().values()) + { + if(field.getPossibleValueSourceName() != null) + { + record.setDisplayValue(field.getName(), translatePossibleValue(field, record.getValue(field.getName()))); + } + } + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + String translatePossibleValue(QFieldMetaData field, Serializable value) + { + QPossibleValueSource possibleValueSource = qInstance.getPossibleValueSource(field.getPossibleValueSourceName()); + if(possibleValueSource == null) + { + LOG.error("Missing possible value source named [" + field.getPossibleValueSourceName() + "] when formatting value for field [" + field.getName() + "]"); + return (null); + } + + // todo - memoize!!! + // todo - bulk!!! + + String resultValue = null; + if(possibleValueSource.getType().equals(QPossibleValueSourceType.ENUM)) + { + resultValue = translatePossibleValueEnum(value, possibleValueSource); + } + else if(possibleValueSource.getType().equals(QPossibleValueSourceType.TABLE)) + { + resultValue = translatePossibleValueTable(field, value, possibleValueSource); + } + else if(possibleValueSource.getType().equals(QPossibleValueSourceType.CUSTOM)) + { + resultValue = translatePossibleValueCustom(field, value, possibleValueSource); + } + else + { + LOG.error("Unrecognized possibleValueSourceType [" + possibleValueSource.getType() + "] in PVS named [" + possibleValueSource.getName() + "] on field [" + field.getName() + "]"); + } + + if(resultValue == null) + { + resultValue = getDefaultForPossibleValue(possibleValueSource, value); + } + + return (resultValue); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private String translatePossibleValueCustom(QFieldMetaData field, Serializable value, QPossibleValueSource possibleValueSource) + { + try + { + Class codeClass = Class.forName(possibleValueSource.getCustomCodeReference().getName()); + Object codeObject = codeClass.getConstructor().newInstance(); + if(!(codeObject instanceof QCustomPossibleValueProvider customPossibleValueProvider)) + { + throw (new QException("The supplied code [" + codeClass.getName() + "] is not an instance of QCustomPossibleValueProvider")); + } + + return (formatPossibleValue(possibleValueSource, customPossibleValueProvider.getPossibleValue(value))); + } + catch(Exception e) + { + LOG.warn("Error sending [" + value + "] for field [" + field + "] through custom code for PVS [" + field.getPossibleValueSourceName() + "]", e); + } + + return (null); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private String translatePossibleValueTable(QFieldMetaData field, Serializable value, QPossibleValueSource possibleValueSource) + { + ///////////////////////////////// + // null input gets null output // + ///////////////////////////////// + if(value == null) + { + return (null); + } + + ////////////////////////////////////////////////////////////// + // look for cached value - if it's missing, call the primer // + ////////////////////////////////////////////////////////////// + possibleValueCache.putIfAbsent(possibleValueSource.getName(), new HashMap<>()); + Map cacheForPvs = possibleValueCache.get(possibleValueSource.getName()); + if(!cacheForPvs.containsKey(value)) + { + primePvsCache(possibleValueSource.getTableName(), List.of(possibleValueSource), List.of(value)); + } + + return (cacheForPvs.get(value)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private String formatPossibleValue(QPossibleValueSource possibleValueSource, QPossibleValue possibleValue) + { + return (doFormatPossibleValue(possibleValueSource.getValueFormat(), possibleValueSource.getValueFields(), possibleValue.getId(), possibleValue.getLabel())); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private String getDefaultForPossibleValue(QPossibleValueSource possibleValueSource, Serializable value) + { + if(possibleValueSource.getValueFormatIfNotFound() == null) + { + return (null); + } + + return (doFormatPossibleValue(possibleValueSource.getValueFormatIfNotFound(), possibleValueSource.getValueFieldsIfNotFound(), value, null)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @SuppressWarnings("checkstyle:Indentation") + private String doFormatPossibleValue(String formatString, List valueFields, Object id, String label) + { + List values = new ArrayList<>(); + if(valueFields != null) + { + for(String valueField : valueFields) + { + Object value = switch(valueField) + { + case "id" -> id; + case "label" -> label; + default -> throw new IllegalArgumentException("Unexpected value field: " + valueField); + }; + values.add(Objects.requireNonNullElse(value, "")); + } + } + + return (formatString.formatted(values.toArray())); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private String translatePossibleValueEnum(Serializable value, QPossibleValueSource possibleValueSource) + { + for(QPossibleValue possibleValue : possibleValueSource.getEnumValues()) + { + if(possibleValue.getId().equals(value)) + { + return (formatPossibleValue(possibleValueSource, possibleValue)); + } + } + + return (null); + } + + + + /******************************************************************************* + ** prime the cache (e.g., by doing bulk-queries) for table-based PVS's + *******************************************************************************/ + void primePvsCache(QTableMetaData table, List records) + { + ListingHash fieldsByPvsTable = new ListingHash<>(); + ListingHash pvsesByTable = new ListingHash<>(); + for(QFieldMetaData field : table.getFields().values()) + { + QPossibleValueSource possibleValueSource = qInstance.getPossibleValueSource(field.getPossibleValueSourceName()); + if(possibleValueSource != null && possibleValueSource.getType().equals(QPossibleValueSourceType.TABLE)) + { + fieldsByPvsTable.add(possibleValueSource.getTableName(), field); + pvsesByTable.add(possibleValueSource.getTableName(), possibleValueSource); + } + } + + for(String tableName : fieldsByPvsTable.keySet()) + { + Set values = new HashSet<>(); + for(QRecord record : records) + { + for(QFieldMetaData field : fieldsByPvsTable.get(tableName)) + { + values.add(record.getValue(field.getName())); + } + } + + primePvsCache(tableName, pvsesByTable.get(tableName), values); + } + } + + + + /******************************************************************************* + ** For a given table, and a list of pkey-values in that table, AND a list of + ** possible value sources based on that table (maybe usually 1, but could be more, + ** e.g., if they had different formatting, or different filters (todo, would that work?) + ** - query for the values in the table, and populate the possibleValueCache. + *******************************************************************************/ + private void primePvsCache(String tableName, List possibleValueSources, Collection values) + { + for(QPossibleValueSource possibleValueSource : possibleValueSources) + { + possibleValueCache.putIfAbsent(possibleValueSource.getName(), new HashMap<>()); + } + + try + { + String primaryKeyField = qInstance.getTable(tableName).getPrimaryKeyField(); + + for(List page : CollectionUtils.getPages(values, 1000)) + { + QueryInput queryInput = new QueryInput(qInstance); + queryInput.setSession(session); + queryInput.setTableName(tableName); + queryInput.setFilter(new QQueryFilter().withCriteria(new QFilterCriteria(primaryKeyField, QCriteriaOperator.IN, page))); + + ///////////////////////////////////////////////////////////////////////////////////////// + // this is needed to get record labels, which are what we use here... unclear if best! // + ///////////////////////////////////////////////////////////////////////////////////////// + queryInput.setShouldGenerateDisplayValues(true); + + QueryOutput queryOutput = new QueryAction().execute(queryInput); + + for(QRecord record : queryOutput.getRecords()) + { + Serializable pkeyValue = record.getValue(primaryKeyField); + for(QPossibleValueSource possibleValueSource : possibleValueSources) + { + QPossibleValue possibleValue = new QPossibleValue<>(pkeyValue, record.getRecordLabel()); + possibleValueCache.get(possibleValueSource.getName()).put(pkeyValue, formatPossibleValue(possibleValueSource, possibleValue)); + } + } + } + } + catch(Exception e) + { + LOG.warn("Error looking up possible values for table [" + tableName + "]", e); + } + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QValueFormatter.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QValueFormatter.java index 4c20ae7f..27ccde27 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QValueFormatter.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QValueFormatter.java @@ -25,8 +25,10 @@ package com.kingsrook.qqq.backend.core.actions.values; import java.io.Serializable; import java.util.List; import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.model.session.QSession; import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.core.utils.ValueUtils; import org.apache.logging.log4j.LogManager; @@ -34,7 +36,8 @@ import org.apache.logging.log4j.Logger; /******************************************************************************* - ** Utility to apply display formats to values for fields + ** Utility to apply display formats to values for records and fields. + ** Note that this includes handling PossibleValues. *******************************************************************************/ public class QValueFormatter { @@ -45,7 +48,16 @@ public class QValueFormatter /******************************************************************************* ** *******************************************************************************/ - public static String formatValue(QFieldMetaData field, Serializable value) + public QValueFormatter() + { + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public String formatValue(QFieldMetaData field, Serializable value) { ////////////////////////////////// // null values get null results // @@ -55,6 +67,16 @@ public class QValueFormatter return (null); } + // todo - is this appropriate, with this class and possibleValueTransaltor being decoupled - to still do standard formatting here? + // alternatively, shold we return null here? + // /////////////////////////////////////////////// + // // if the field has a possible value, use it // + // /////////////////////////////////////////////// + // if(field.getPossibleValueSourceName() != null) + // { + // return (this.possibleValueTranslator.translatePossibleValue(field, value)); + // } + //////////////////////////////////////////////////////// // if the field has a display format, try to apply it // //////////////////////////////////////////////////////// @@ -68,6 +90,7 @@ public class QValueFormatter { try { + // todo - revisit if we actually want this - or - if you should get an error if you mis-configure your table this way (ideally during validation!) if(e.getMessage().equals("f != java.lang.Integer")) { return formatValue(field, ValueUtils.getValueAsBigDecimal(value)); @@ -99,7 +122,7 @@ public class QValueFormatter /******************************************************************************* ** Make a string from a table's recordLabelFormat and fields, for a given record. *******************************************************************************/ - public static String formatRecordLabel(QTableMetaData table, QRecord record) + public String formatRecordLabel(QTableMetaData table, QRecord record) { if(!StringUtils.hasContent(table.getRecordLabelFormat())) { @@ -128,7 +151,7 @@ public class QValueFormatter /******************************************************************************* ** Deal with non-happy-path cases for making a record label. *******************************************************************************/ - private static String formatRecordLabelExceptionalCases(QTableMetaData table, QRecord record) + private String formatRecordLabelExceptionalCases(QTableMetaData table, QRecord record) { /////////////////////////////////////////////////////////////////////////////////////// // if there's no record label format, then just return the primary key display value // @@ -156,7 +179,7 @@ public class QValueFormatter /******************************************************************************* ** For a list of records, set their recordLabels and display values *******************************************************************************/ - public static void setDisplayValuesInRecords(QTableMetaData table, List records) + public void setDisplayValuesInRecords(QTableMetaData table, List records) { if(records == null) { @@ -167,12 +190,13 @@ public class QValueFormatter { for(QFieldMetaData field : table.getFields().values()) { - String formattedValue = QValueFormatter.formatValue(field, record.getValue(field.getName())); + String formattedValue = formatValue(field, record.getValue(field.getName())); record.setDisplayValue(field.getName(), formattedValue); } - record.setRecordLabel(QValueFormatter.formatRecordLabel(table, record)); + record.setRecordLabel(formatRecordLabel(table, record)); } } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/adapters/CsvToQRecordAdapter.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/adapters/CsvToQRecordAdapter.java index 3d5493a3..98f85478 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/adapters/CsvToQRecordAdapter.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/adapters/CsvToQRecordAdapter.java @@ -95,6 +95,15 @@ public class CsvToQRecordAdapter throw (new IllegalArgumentException("Empty csv value was provided.")); } + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // once, from a DOS csv file (that had come from Excel), we had a "" character (FEFF, Byte-order marker) at the start of a // + // CSV, which caused our first header to not match... So, let us strip away any FEFF or FFFE's at the start of CSV strings. // + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + if(csv.length() > 1 && (csv.charAt(0) == 0xfeff || csv.charAt(0) == 0xfffe)) + { + csv = csv.substring(1); + } + try { /////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -118,7 +127,7 @@ public class CsvToQRecordAdapter // put values from the CSV record into a map of header -> value // ////////////////////////////////////////////////////////////////// Map csvValues = new HashMap<>(); - for(int i = 0; i < headers.size(); i++) + for(int i = 0; i < headers.size() && i < csvRecord.size(); i++) { csvValues.put(headers.get(i), csvRecord.get(i)); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java index 6104f9db..119daa34 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java @@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.instances; import java.util.ArrayList; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Locale; @@ -128,6 +129,11 @@ public class QInstanceEnricher { generateTableFieldSections(table); } + + if(CollectionUtils.nullSafeHasContents(table.getRecordLabelFields()) && !StringUtils.hasContent(table.getRecordLabelFormat())) + { + table.setRecordLabelFormat(String.join(" ", Collections.nCopies(table.getRecordLabelFields().size(), "%s"))); + } } @@ -211,7 +217,7 @@ public class QInstanceEnricher /******************************************************************************* ** *******************************************************************************/ - private String nameToLabel(String name) + static String nameToLabel(String name) { if(!StringUtils.hasContent(name)) { @@ -223,7 +229,7 @@ public class QInstanceEnricher return (name.substring(0, 1).toUpperCase(Locale.ROOT)); } - return (name.substring(0, 1).toUpperCase(Locale.ROOT) + name.substring(1).replaceAll("([A-Z])", " $1")); + return (name.substring(0, 1).toUpperCase(Locale.ROOT) + name.substring(1).replaceAll("([A-Z0-9]+)", " $1").replaceAll("([0-9])([A-Za-z])", "$1 $2")); } @@ -579,7 +585,7 @@ public class QInstanceEnricher { for(String fieldName : table.getRecordLabelFields()) { - if(!usedFieldNames.contains(fieldName)) + if(!usedFieldNames.contains(fieldName) && table.getFields().containsKey(fieldName)) { identitySection.getFieldNames().add(fieldName); usedFieldNames.add(fieldName); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java index 9ae87598..66959a31 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java @@ -29,6 +29,7 @@ import java.util.Objects; import java.util.Set; import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeUsage; import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppChildMetaData; import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData; @@ -88,6 +89,7 @@ public class QInstanceValidator validateTables(qInstance, errors); validateProcesses(qInstance, errors); validateApps(qInstance, errors); + validatePossibleValueSources(qInstance, errors); } catch(Exception e) { @@ -167,8 +169,8 @@ public class QInstanceValidator ////////////////////////////////////////// // validate field sections in the table // ////////////////////////////////////////// - Set fieldNamesInSections = new HashSet<>(); - QFieldSection tier1Section = null; + Set fieldNamesInSections = new HashSet<>(); + QFieldSection tier1Section = null; if(table.getSections() != null) { for(QFieldSection section : table.getSections()) @@ -190,6 +192,16 @@ public class QInstanceValidator } } + /////////////////////////////// + // validate the record label // + /////////////////////////////// + if(table.getRecordLabelFields() != null) + { + for(String recordLabelField : table.getRecordLabelFields()) + { + assertCondition(errors, table.getFields().containsKey(recordLabelField), "Table " + tableName + " record label field " + recordLabelField + " is not a field on this table."); + } + } }); } } @@ -225,7 +237,7 @@ public class QInstanceValidator *******************************************************************************/ private void validateProcesses(QInstance qInstance, List errors) { - if(!CollectionUtils.nullSafeIsEmpty(qInstance.getProcesses())) + if(CollectionUtils.nullSafeHasContents(qInstance.getProcesses())) { qInstance.getProcesses().forEach((processName, process) -> { @@ -264,7 +276,7 @@ public class QInstanceValidator *******************************************************************************/ private void validateApps(QInstance qInstance, List errors) { - if(!CollectionUtils.nullSafeIsEmpty(qInstance.getApps())) + if(CollectionUtils.nullSafeHasContents(qInstance.getApps())) { qInstance.getApps().forEach((appName, app) -> { @@ -291,6 +303,61 @@ public class QInstanceValidator + /******************************************************************************* + ** + *******************************************************************************/ + private void validatePossibleValueSources(QInstance qInstance, List errors) + { + if(CollectionUtils.nullSafeHasContents(qInstance.getPossibleValueSources())) + { + qInstance.getPossibleValueSources().forEach((pvsName, possibleValueSource) -> + { + assertCondition(errors, Objects.equals(pvsName, possibleValueSource.getName()), "Inconsistent naming for possibleValueSource: " + pvsName + "/" + possibleValueSource.getName() + "."); + assertCondition(errors, possibleValueSource.getIdType() != null, "Missing an idType for possibleValueSource: " + pvsName); + if(assertCondition(errors, possibleValueSource.getType() != null, "Missing type for possibleValueSource: " + pvsName)) + { + //////////////////////////////////////////////////////////////////////////////////////////////// + // assert about fields that should and should not be set, based on possible value source type // + // do additional type-specific validations as well // + //////////////////////////////////////////////////////////////////////////////////////////////// + switch(possibleValueSource.getType()) + { + case ENUM -> + { + assertCondition(errors, !StringUtils.hasContent(possibleValueSource.getTableName()), "enum-type possibleValueSource " + pvsName + " should not have a tableName."); + assertCondition(errors, possibleValueSource.getCustomCodeReference() == null, "enum-type possibleValueSource " + pvsName + " should not have a customCodeReference."); + + assertCondition(errors, CollectionUtils.nullSafeHasContents(possibleValueSource.getEnumValues()), "enum-type possibleValueSource " + pvsName + " is missing enum values"); + } + case TABLE -> + { + assertCondition(errors, CollectionUtils.nullSafeIsEmpty(possibleValueSource.getEnumValues()), "table-type possibleValueSource " + pvsName + " should not have enum values."); + assertCondition(errors, possibleValueSource.getCustomCodeReference() == null, "table-type possibleValueSource " + pvsName + " should not have a customCodeReference."); + + if(assertCondition(errors, StringUtils.hasContent(possibleValueSource.getTableName()), "table-type possibleValueSource " + pvsName + " is missing a tableName.")) + { + assertCondition(errors, qInstance.getTable(possibleValueSource.getTableName()) != null, "Unrecognized table " + possibleValueSource.getTableName() + " for possibleValueSource " + pvsName + "."); + } + } + case CUSTOM -> + { + assertCondition(errors, CollectionUtils.nullSafeIsEmpty(possibleValueSource.getEnumValues()), "custom-type possibleValueSource " + pvsName + " should not have enum values."); + assertCondition(errors, !StringUtils.hasContent(possibleValueSource.getTableName()), "custom-type possibleValueSource " + pvsName + " should not have a tableName."); + + if(assertCondition(errors, possibleValueSource.getCustomCodeReference() != null, "custom-type possibleValueSource " + pvsName + " is missing a customCodeReference.")) + { + assertCondition(errors, QCodeUsage.POSSIBLE_VALUE_PROVIDER.equals(possibleValueSource.getCustomCodeReference().getCodeUsage()), "customCodeReference for possibleValueSource " + pvsName + " is not a possibleValueProvider."); + } + } + default -> errors.add("Unexpected possibleValueSource type: " + possibleValueSource.getType()); + } + } + }); + } + } + + + /******************************************************************************* ** Check if an app's child list can recursively be traversed without finding a ** duplicate, which would indicate a cycle (e.g., an error) diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryInput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryInput.java index daaa5ab7..94c2a7ae 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryInput.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryInput.java @@ -40,6 +40,8 @@ public class QueryInput extends AbstractTableActionInput private RecordPipe recordPipe; + private boolean shouldTranslatePossibleValues = false; + private boolean shouldGenerateDisplayValues = false; /******************************************************************************* @@ -158,4 +160,47 @@ public class QueryInput extends AbstractTableActionInput this.recordPipe = recordPipe; } + + + /******************************************************************************* + ** Getter for shouldTranslatePossibleValues + ** + *******************************************************************************/ + public boolean getShouldTranslatePossibleValues() + { + return shouldTranslatePossibleValues; + } + + + + /******************************************************************************* + ** Setter for shouldTranslatePossibleValues + ** + *******************************************************************************/ + public void setShouldTranslatePossibleValues(boolean shouldTranslatePossibleValues) + { + this.shouldTranslatePossibleValues = shouldTranslatePossibleValues; + } + + + + /******************************************************************************* + ** Getter for shouldGenerateDisplayValues + ** + *******************************************************************************/ + public boolean getShouldGenerateDisplayValues() + { + return shouldGenerateDisplayValues; + } + + + + /******************************************************************************* + ** Setter for shouldGenerateDisplayValues + ** + *******************************************************************************/ + public void setShouldGenerateDisplayValues(boolean shouldGenerateDisplayValues) + { + this.shouldGenerateDisplayValues = shouldGenerateDisplayValues; + } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QField.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QField.java index 6fd3eeb6..1988bd6a 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QField.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QField.java @@ -62,6 +62,11 @@ public @interface QField *******************************************************************************/ String displayFormat() default ""; + /******************************************************************************* + ** + *******************************************************************************/ + String possibleValueSourceName() default ""; + ////////////////////////////////////////////////////////////////////////////////////////// // new attributes here likely need implementation in QFieldMetaData.constructFromGetter // ////////////////////////////////////////////////////////////////////////////////////////// diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QInstance.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QInstance.java index 6f4c5230..3712d077 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QInstance.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QInstance.java @@ -54,10 +54,10 @@ public class QInstance //////////////////////////////////////////////////////////////////////////////////////////// // Important to use LinkedHashmap here, to preserve the order in which entries are added. // //////////////////////////////////////////////////////////////////////////////////////////// - private Map tables = new LinkedHashMap<>(); - private Map> possibleValueSources = new LinkedHashMap<>(); - private Map processes = new LinkedHashMap<>(); - private Map apps = new LinkedHashMap<>(); + private Map tables = new LinkedHashMap<>(); + private Map possibleValueSources = new LinkedHashMap<>(); + private Map processes = new LinkedHashMap<>(); + private Map apps = new LinkedHashMap<>(); // todo - lock down the object (no more changes allowed) after it's been validated? @@ -190,7 +190,7 @@ public class QInstance /******************************************************************************* ** *******************************************************************************/ - public void addPossibleValueSource(QPossibleValueSource possibleValueSource) + public void addPossibleValueSource(QPossibleValueSource possibleValueSource) { this.addPossibleValueSource(possibleValueSource.getName(), possibleValueSource); } @@ -353,7 +353,7 @@ public class QInstance ** Getter for possibleValueSources ** *******************************************************************************/ - public Map> getPossibleValueSources() + public Map getPossibleValueSources() { return possibleValueSources; } @@ -364,7 +364,7 @@ public class QInstance ** Setter for possibleValueSources ** *******************************************************************************/ - public void setPossibleValueSources(Map> possibleValueSources) + public void setPossibleValueSources(Map possibleValueSources) { this.possibleValueSources = possibleValueSources; } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/QCodeReference.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/QCodeReference.java index c1093ca9..5a82f004 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/QCodeReference.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/QCodeReference.java @@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.model.metadata.code; import com.kingsrook.qqq.backend.core.actions.processes.BackendStep; +import com.kingsrook.qqq.backend.core.actions.values.QCustomPossibleValueProvider; /******************************************************************************* @@ -70,6 +71,10 @@ public class QCodeReference { this.codeUsage = QCodeUsage.BACKEND_STEP; } + else if(QCustomPossibleValueProvider.class.isAssignableFrom(javaClass)) + { + this.codeUsage = QCodeUsage.POSSIBLE_VALUE_PROVIDER; + } else { throw (new IllegalStateException("Unable to infer code usage type for class: " + javaClass.getName())); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/QCodeUsage.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/QCodeUsage.java index 9b459af1..925c1f55 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/QCodeUsage.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/QCodeUsage.java @@ -29,5 +29,6 @@ package com.kingsrook.qqq.backend.core.model.metadata.code; public enum QCodeUsage { BACKEND_STEP, // a backend-step in a process - CUSTOMIZER // a function to customize part of a QQQ table's behavior + CUSTOMIZER, // a function to customize part of a QQQ table's behavior + POSSIBLE_VALUE_PROVIDER // code that drives a custom possibleValueSource } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldMetaData.java index 8967c579..a21d7cce 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldMetaData.java @@ -133,6 +133,11 @@ public class QFieldMetaData { setDisplayFormat(fieldAnnotation.displayFormat()); } + + if(StringUtils.hasContent(fieldAnnotation.possibleValueSourceName())) + { + setPossibleValueSourceName(fieldAnnotation.possibleValueSourceName()); + } } } catch(QException qe) @@ -406,6 +411,7 @@ public class QFieldMetaData } + /******************************************************************************* ** Getter for displayFormat ** @@ -427,6 +433,7 @@ public class QFieldMetaData } + /******************************************************************************* ** Fluent setter for displayFormat ** diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendFieldMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendFieldMetaData.java index f57a05ff..4dbbcfc6 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendFieldMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendFieldMetaData.java @@ -36,11 +36,13 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; @JsonInclude(Include.NON_NULL) public class QFrontendFieldMetaData { - private String name; - private String label; + private String name; + private String label; private QFieldType type; - private boolean isRequired; - private boolean isEditable; + private boolean isRequired; + private boolean isEditable; + private String possibleValueSourceName; + private String displayFormat; ////////////////////////////////////////////////////////////////////////////////// // do not add setters. take values from the source-object in the constructor!! // @@ -58,6 +60,8 @@ public class QFrontendFieldMetaData this.type = fieldMetaData.getType(); this.isRequired = fieldMetaData.getIsRequired(); this.isEditable = fieldMetaData.getIsEditable(); + this.possibleValueSourceName = fieldMetaData.getPossibleValueSourceName(); + this.displayFormat = fieldMetaData.getDisplayFormat(); } @@ -115,4 +119,26 @@ public class QFrontendFieldMetaData return isEditable; } + + + /******************************************************************************* + ** Getter for displayFormat + ** + *******************************************************************************/ + public String getDisplayFormat() + { + return displayFormat; + } + + + + /******************************************************************************* + ** Getter for possibleValueSourceName + ** + *******************************************************************************/ + public String getPossibleValueSourceName() + { + return possibleValueSourceName; + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/possiblevalues/PossibleValueEnum.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/possiblevalues/PossibleValueEnum.java new file mode 100644 index 00000000..24018b3a --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/possiblevalues/PossibleValueEnum.java @@ -0,0 +1,39 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.model.metadata.possiblevalues; + + +/******************************************************************************* + ** + *******************************************************************************/ +public interface PossibleValueEnum +{ + /******************************************************************************* + ** + *******************************************************************************/ + T getPossibleValueId(); + + /******************************************************************************* + ** + *******************************************************************************/ + String getPossibleValueLabel(); +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/possiblevalues/QPossibleValue.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/possiblevalues/QPossibleValue.java new file mode 100644 index 00000000..9bdae034 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/possiblevalues/QPossibleValue.java @@ -0,0 +1,78 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.model.metadata.possiblevalues; + + +/******************************************************************************* + ** An actual possible value - an id and label. + ** + *******************************************************************************/ +public class QPossibleValue +{ + private final T id; + private final String label; + + + + /******************************************************************************* + ** + *******************************************************************************/ + @SuppressWarnings("unchecked") + public QPossibleValue(String value) + { + this.id = (T) value; + this.label = value; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public QPossibleValue(T id, String label) + { + this.id = id; + this.label = label; + } + + + + /******************************************************************************* + ** Getter for id + ** + *******************************************************************************/ + public T getId() + { + return id; + } + + + + /******************************************************************************* + ** Getter for label + ** + *******************************************************************************/ + public String getLabel() + { + return label; + } +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/possiblevalues/QPossibleValueSource.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/possiblevalues/QPossibleValueSource.java index 0cb0a322..735a22e4 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/possiblevalues/QPossibleValueSource.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/possiblevalues/QPossibleValueSource.java @@ -24,19 +24,62 @@ package com.kingsrook.qqq.backend.core.model.metadata.possiblevalues; import java.util.ArrayList; import java.util.List; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; /******************************************************************************* ** Meta-data to represent a single field in a table. ** *******************************************************************************/ -public class QPossibleValueSource +public class QPossibleValueSource { - private String name; + private String name; private QPossibleValueSourceType type; + private QFieldType idType = QFieldType.INTEGER; - // should these be in sub-types?? - private List enumValues; + private String valueFormat = ValueFormat.DEFAULT; + private List valueFields = ValueFields.DEFAULT; + private String valueFormatIfNotFound = null; + private List valueFieldsIfNotFound = null; + + + + public interface ValueFormat + { + String DEFAULT = "%s"; + String LABEL_ONLY = "%s"; + String LABEL_PARENS_ID = "%s (%s)"; + String ID_COLON_LABEL = "%s: %s"; + } + + + + public interface ValueFields + { + List DEFAULT = List.of("label"); + List LABEL_ONLY = List.of("label"); + List LABEL_PARENS_ID = List.of("label", "id"); + List ID_COLON_LABEL = List.of("id", "label"); + } + + // todo - optimization hints, such as "table is static, fully cache" or "table is small, so we can pull the whole thing into memory?" + + ////////////////////// + // for type = TABLE // + ////////////////////// + private String tableName; + // todo - override labelFormat & labelFields? + + ///////////////////// + // for type = ENUM // + ///////////////////// + private List> enumValues; + + /////////////////////// + // for type = CUSTOM // + /////////////////////// + private QCodeReference customCodeReference; @@ -72,7 +115,7 @@ public class QPossibleValueSource /******************************************************************************* ** *******************************************************************************/ - public QPossibleValueSource withName(String name) + public QPossibleValueSource withName(String name) { this.name = name; return (this); @@ -103,7 +146,7 @@ public class QPossibleValueSource /******************************************************************************* ** *******************************************************************************/ - public QPossibleValueSource withType(QPossibleValueSourceType type) + public QPossibleValueSource withType(QPossibleValueSourceType type) { this.type = type; return (this); @@ -111,11 +154,215 @@ public class QPossibleValueSource + /******************************************************************************* + ** Getter for idType + ** + *******************************************************************************/ + public QFieldType getIdType() + { + return idType; + } + + + + /******************************************************************************* + ** Setter for idType + ** + *******************************************************************************/ + public void setIdType(QFieldType idType) + { + this.idType = idType; + } + + + + /******************************************************************************* + ** Fluent setter for idType + ** + *******************************************************************************/ + public QPossibleValueSource withIdType(QFieldType idType) + { + this.idType = idType; + return (this); + } + + + + /******************************************************************************* + ** Getter for valueFormat + ** + *******************************************************************************/ + public String getValueFormat() + { + return valueFormat; + } + + + + /******************************************************************************* + ** Setter for valueFormat + ** + *******************************************************************************/ + public void setValueFormat(String valueFormat) + { + this.valueFormat = valueFormat; + } + + + + /******************************************************************************* + ** Fluent setter for valueFormat + ** + *******************************************************************************/ + public QPossibleValueSource withValueFormat(String valueFormat) + { + this.valueFormat = valueFormat; + return (this); + } + + + + /******************************************************************************* + ** Getter for valueFields + ** + *******************************************************************************/ + public List getValueFields() + { + return valueFields; + } + + + + /******************************************************************************* + ** Setter for valueFields + ** + *******************************************************************************/ + public void setValueFields(List valueFields) + { + this.valueFields = valueFields; + } + + + + /******************************************************************************* + ** Fluent setter for valueFields + ** + *******************************************************************************/ + public QPossibleValueSource withValueFields(List valueFields) + { + this.valueFields = valueFields; + return (this); + } + + + + /******************************************************************************* + ** Getter for valueFormatIfNotFound + ** + *******************************************************************************/ + public String getValueFormatIfNotFound() + { + return valueFormatIfNotFound; + } + + + + /******************************************************************************* + ** Setter for valueFormatIfNotFound + ** + *******************************************************************************/ + public void setValueFormatIfNotFound(String valueFormatIfNotFound) + { + this.valueFormatIfNotFound = valueFormatIfNotFound; + } + + + + /******************************************************************************* + ** Fluent setter for valueFormatIfNotFound + ** + *******************************************************************************/ + public QPossibleValueSource withValueFormatIfNotFound(String valueFormatIfNotFound) + { + this.valueFormatIfNotFound = valueFormatIfNotFound; + return (this); + } + + + + /******************************************************************************* + ** Getter for valueFieldsIfNotFound + ** + *******************************************************************************/ + public List getValueFieldsIfNotFound() + { + return valueFieldsIfNotFound; + } + + + + /******************************************************************************* + ** Setter for valueFieldsIfNotFound + ** + *******************************************************************************/ + public void setValueFieldsIfNotFound(List valueFieldsIfNotFound) + { + this.valueFieldsIfNotFound = valueFieldsIfNotFound; + } + + + + /******************************************************************************* + ** Fluent setter for valueFieldsIfNotFound + ** + *******************************************************************************/ + public QPossibleValueSource withValueFieldsIfNotFound(List valueFieldsIfNotFound) + { + this.valueFieldsIfNotFound = valueFieldsIfNotFound; + return (this); + } + + + + /******************************************************************************* + ** Getter for tableName + ** + *******************************************************************************/ + public String getTableName() + { + return tableName; + } + + + + /******************************************************************************* + ** Setter for tableName + ** + *******************************************************************************/ + public void setTableName(String tableName) + { + this.tableName = tableName; + } + + + + /******************************************************************************* + ** Fluent setter for tableName + ** + *******************************************************************************/ + public QPossibleValueSource withTableName(String tableName) + { + this.tableName = tableName; + return (this); + } + + + /******************************************************************************* ** Getter for enumValues ** *******************************************************************************/ - public List getEnumValues() + public List> getEnumValues() { return enumValues; } @@ -126,7 +373,7 @@ public class QPossibleValueSource ** Setter for enumValues ** *******************************************************************************/ - public void setEnumValues(List enumValues) + public void setEnumValues(List> enumValues) { this.enumValues = enumValues; } @@ -137,7 +384,7 @@ public class QPossibleValueSource ** Fluent setter for enumValues ** *******************************************************************************/ - public QPossibleValueSource withEnumValues(List enumValues) + public QPossibleValueSource withEnumValues(List> enumValues) { this.enumValues = enumValues; return this; @@ -146,16 +393,64 @@ public class QPossibleValueSource /******************************************************************************* - ** Fluent adder for enumValues ** *******************************************************************************/ - public QPossibleValueSource addEnumValue(T enumValue) + public void addEnumValue(QPossibleValue possibleValue) { if(this.enumValues == null) { this.enumValues = new ArrayList<>(); } - this.enumValues.add(enumValue); - return this; + this.enumValues.add(possibleValue); } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public > QPossibleValueSource withValuesFromEnum(T[] values) + { + for(T t : values) + { + addEnumValue(new QPossibleValue<>(t.getPossibleValueId(), t.getPossibleValueLabel())); + } + + return (this); + } + + + + /******************************************************************************* + ** Getter for customCodeReference + ** + *******************************************************************************/ + public QCodeReference getCustomCodeReference() + { + return customCodeReference; + } + + + + /******************************************************************************* + ** Setter for customCodeReference + ** + *******************************************************************************/ + public void setCustomCodeReference(QCodeReference customCodeReference) + { + this.customCodeReference = customCodeReference; + } + + + + /******************************************************************************* + ** Fluent setter for customCodeReference + ** + *******************************************************************************/ + public QPossibleValueSource withCustomCodeReference(QCodeReference customCodeReference) + { + this.customCodeReference = customCodeReference; + return (this); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QTableMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QTableMetaData.java index 2f920886..ebb3e36a 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QTableMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QTableMetaData.java @@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.core.model.metadata.tables; import java.io.Serializable; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; @@ -587,6 +588,18 @@ public class QTableMetaData implements QAppChildMetaData, Serializable + /******************************************************************************* + ** Fluent setter for recordLabelFields + ** + *******************************************************************************/ + public QTableMetaData withRecordLabelFields(String... recordLabelFields) + { + this.recordLabelFields = Arrays.asList(recordLabelFields); + return (this); + } + + + /******************************************************************************* ** Getter for sections ** diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/Auth0AuthenticationModule.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/Auth0AuthenticationModule.java index 435a13ce..6b53dd6b 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/Auth0AuthenticationModule.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/Auth0AuthenticationModule.java @@ -167,6 +167,11 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface return (false); } + if(session.getIdReference() == null) + { + return (false); + } + StateProviderInterface spi = getStateProvider(); Auth0StateKey key = new Auth0StateKey(session.getIdReference()); Optional lastTimeCheckedOptional = spi.get(Instant.class, key); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java index 8fc7a02b..f195edcf 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java @@ -24,18 +24,21 @@ package com.kingsrook.qqq.backend.core.modules.backend.implementations.memory; import java.io.Serializable; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput; import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput; import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput; import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput; import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import org.apache.commons.lang.NotImplementedException; /******************************************************************************* @@ -48,6 +51,12 @@ public class MemoryRecordStore private Map> data; private Map nextSerials; + private static boolean collectStatistics = false; + + private static final Map statistics = Collections.synchronizedMap(new HashMap<>()); + + public static final String STAT_QUERIES_RAN = "queriesRan"; + /******************************************************************************* @@ -105,9 +114,56 @@ public class MemoryRecordStore *******************************************************************************/ public List query(QueryInput input) { + incrementStatistic(STAT_QUERIES_RAN); + Map tableData = getTableData(input.getTable()); - List records = new ArrayList<>(tableData.values()); - // todo - filtering + List records = new ArrayList<>(); + + for(QRecord qRecord : tableData.values()) + { + boolean recordMatches = true; + if(input.getFilter() != null && input.getFilter().getCriteria() != null) + { + for(QFilterCriteria criterion : input.getFilter().getCriteria()) + { + String fieldName = criterion.getFieldName(); + Serializable value = qRecord.getValue(fieldName); + switch(criterion.getOperator()) + { + case EQUALS: + { + if(!value.equals(criterion.getValues().get(0))) + { + recordMatches = false; + } + break; + } + case IN: + { + if(!criterion.getValues().contains(value)) + { + recordMatches = false; + } + break; + } + default: + { + throw new NotImplementedException("Operator [" + criterion.getOperator() + "] is not yet implemented in the Memory backend."); + } + } + if(!recordMatches) + { + break; + } + } + } + + if(recordMatches) + { + records.add(qRecord); + } + } + return (records); } @@ -120,7 +176,7 @@ public class MemoryRecordStore { Map tableData = getTableData(input.getTable()); List records = new ArrayList<>(tableData.values()); - // todo - filtering + // todo - filtering (call query) return (records.size()); } @@ -235,4 +291,53 @@ public class MemoryRecordStore return (rowsDeleted); } + + + + /******************************************************************************* + ** Setter for collectStatistics + ** + *******************************************************************************/ + public static void setCollectStatistics(boolean collectStatistics) + { + MemoryRecordStore.collectStatistics = collectStatistics; + } + + + + /******************************************************************************* + ** Increment a statistic + ** + *******************************************************************************/ + public static void incrementStatistic(String statName) + { + if(collectStatistics) + { + statistics.putIfAbsent(statName, 0); + statistics.put(statName, statistics.get(statName) + 1); + } + } + + + + /******************************************************************************* + ** clear the map of statistics + ** + *******************************************************************************/ + public static void resetStatistics() + { + statistics.clear(); + } + + + + /******************************************************************************* + ** Getter for statistics + ** + *******************************************************************************/ + public static Map getStatistics() + { + return statistics; + } + } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/QueryActionTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/QueryActionTest.java index 326e0742..bdb3b21b 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/QueryActionTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/QueryActionTest.java @@ -47,18 +47,33 @@ class QueryActionTest @Test public void test() throws QException { - QueryInput request = new QueryInput(TestUtils.defineInstance()); - request.setSession(TestUtils.getMockSession()); - request.setTableName("person"); - QueryOutput result = new QueryAction().execute(request); - assertNotNull(result); + QueryInput queryInput = new QueryInput(TestUtils.defineInstance()); + queryInput.setSession(TestUtils.getMockSession()); + queryInput.setTableName("person"); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + assertNotNull(queryOutput); - assertThat(result.getRecords()).isNotEmpty(); - for(QRecord record : result.getRecords()) + assertThat(queryOutput.getRecords()).isNotEmpty(); + for(QRecord record : queryOutput.getRecords()) { assertThat(record.getValues()).isNotEmpty(); - assertThat(record.getDisplayValues()).isNotEmpty(); assertThat(record.getErrors()).isEmpty(); + + /////////////////////////////////////////////////////////////// + // this SHOULD be empty, based on the default for the should // + /////////////////////////////////////////////////////////////// + assertThat(record.getDisplayValues()).isEmpty(); + } + + //////////////////////////////////// + // now flip that field and re-run // + //////////////////////////////////// + queryInput.setShouldGenerateDisplayValues(true); + assertThat(queryOutput.getRecords()).isNotEmpty(); + queryOutput = new QueryAction().execute(queryInput); + for(QRecord record : queryOutput.getRecords()) + { + assertThat(record.getDisplayValues()).isNotEmpty(); } } } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/values/QPossibleValueTranslatorTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/values/QPossibleValueTranslatorTest.java new file mode 100644 index 00000000..2d4dbbf6 --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/values/QPossibleValueTranslatorTest.java @@ -0,0 +1,241 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.actions.values; + + +import java.math.BigDecimal; +import java.util.Collections; +import java.util.List; +import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; +import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.fields.DisplayFormat; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; +import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.model.session.QSession; +import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryRecordStore; +import com.kingsrook.qqq.backend.core.utils.TestUtils; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class QPossibleValueTranslatorTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testPossibleValueEnum() + { + QInstance qInstance = TestUtils.defineInstance(); + QPossibleValueTranslator possibleValueTranslator = new QPossibleValueTranslator(qInstance, new QSession()); + QFieldMetaData stateField = qInstance.getTable("person").getField("homeStateId"); + QPossibleValueSource possibleValueSource = qInstance.getPossibleValueSource(stateField.getPossibleValueSourceName()); + + ////////////////////////////////////////////////////////////////////////// + // assert the default formatting for a not-found value is a null string // + ////////////////////////////////////////////////////////////////////////// + assertNull(possibleValueTranslator.translatePossibleValue(stateField, null)); + assertNull(possibleValueTranslator.translatePossibleValue(stateField, -1)); + + ////////////////////////////////////////////////////////////////////// + // let the not-found value be a simple string (no formatted values) // + ////////////////////////////////////////////////////////////////////// + possibleValueSource.setValueFormatIfNotFound("?"); + assertEquals("?", possibleValueTranslator.translatePossibleValue(stateField, null)); + assertEquals("?", possibleValueTranslator.translatePossibleValue(stateField, -1)); + + ///////////////////////////////////////////////////////////// + // let the not-found value be a string w/ formatted values // + ///////////////////////////////////////////////////////////// + possibleValueSource.setValueFormatIfNotFound("? (%s)"); + possibleValueSource.setValueFieldsIfNotFound(List.of("id")); + assertEquals("? ()", possibleValueTranslator.translatePossibleValue(stateField, null)); + assertEquals("? (-1)", possibleValueTranslator.translatePossibleValue(stateField, -1)); + + ///////////////////////////////////////////////////// + // assert the default formatting is just the label // + ///////////////////////////////////////////////////// + assertEquals("MO", possibleValueTranslator.translatePossibleValue(stateField, 2)); + assertEquals("IL", possibleValueTranslator.translatePossibleValue(stateField, 1)); + + ///////////////////////////////////////////////////////////////// + // assert the LABEL_ONLY format (when called out specifically) // + ///////////////////////////////////////////////////////////////// + possibleValueSource.setValueFormat(QPossibleValueSource.ValueFormat.LABEL_ONLY); + possibleValueSource.setValueFields(QPossibleValueSource.ValueFields.LABEL_ONLY); + assertEquals("IL", possibleValueTranslator.translatePossibleValue(stateField, 1)); + + /////////////////////////////////////// + // assert the LABEL_PARAMS_ID format // + /////////////////////////////////////// + possibleValueSource.setValueFormat(QPossibleValueSource.ValueFormat.LABEL_PARENS_ID); + possibleValueSource.setValueFields(QPossibleValueSource.ValueFields.LABEL_PARENS_ID); + assertEquals("IL (1)", possibleValueTranslator.translatePossibleValue(stateField, 1)); + + ////////////////////////////////////// + // assert the ID_COLON_LABEL format // + ////////////////////////////////////// + possibleValueSource.setValueFormat(QPossibleValueSource.ValueFormat.ID_COLON_LABEL); + possibleValueSource.setValueFields(QPossibleValueSource.ValueFields.ID_COLON_LABEL); + assertEquals("1: IL", possibleValueTranslator.translatePossibleValue(stateField, 1)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testPossibleValueTable() throws QException + { + QInstance qInstance = TestUtils.defineInstance(); + QPossibleValueTranslator possibleValueTranslator = new QPossibleValueTranslator(qInstance, new QSession()); + QTableMetaData shapeTable = qInstance.getTable(TestUtils.TABLE_NAME_SHAPE); + QFieldMetaData shapeField = qInstance.getTable(TestUtils.TABLE_NAME_PERSON).getField("favoriteShapeId"); + QPossibleValueSource possibleValueSource = qInstance.getPossibleValueSource(shapeField.getPossibleValueSourceName()); + + List shapeRecords = List.of( + new QRecord().withTableName(shapeTable.getName()).withValue("id", 1).withValue("name", "Triangle"), + new QRecord().withTableName(shapeTable.getName()).withValue("id", 2).withValue("name", "Square"), + new QRecord().withTableName(shapeTable.getName()).withValue("id", 3).withValue("name", "Circle")); + + InsertInput insertInput = new InsertInput(qInstance); + insertInput.setSession(new QSession()); + insertInput.setTableName(shapeTable.getName()); + insertInput.setRecords(shapeRecords); + new InsertAction().execute(insertInput); + + ////////////////////////////////////////////////////////////////////////// + // assert the default formatting for a not-found value is a null string // + ////////////////////////////////////////////////////////////////////////// + assertNull(possibleValueTranslator.translatePossibleValue(shapeField, null)); + assertNull(possibleValueTranslator.translatePossibleValue(shapeField, -1)); + + ////////////////////////////////////////////////////////////////////// + // let the not-found value be a simple string (no formatted values) // + ////////////////////////////////////////////////////////////////////// + possibleValueSource.setValueFormatIfNotFound("?"); + assertEquals("?", possibleValueTranslator.translatePossibleValue(shapeField, null)); + assertEquals("?", possibleValueTranslator.translatePossibleValue(shapeField, -1)); + + ///////////////////////////////////////////////////// + // assert the default formatting is just the label // + ///////////////////////////////////////////////////// + assertEquals("Square", possibleValueTranslator.translatePossibleValue(shapeField, 2)); + assertEquals("Triangle", possibleValueTranslator.translatePossibleValue(shapeField, 1)); + + /////////////////////////////////////// + // assert the LABEL_PARAMS_ID format // + /////////////////////////////////////// + possibleValueSource.setValueFormat(QPossibleValueSource.ValueFormat.LABEL_PARENS_ID); + possibleValueSource.setValueFields(QPossibleValueSource.ValueFields.LABEL_PARENS_ID); + assertEquals("Circle (3)", possibleValueTranslator.translatePossibleValue(shapeField, 3)); + + /////////////////////////////////////////////////////////// + // assert that we don't re-run queries for cached values // + /////////////////////////////////////////////////////////// + possibleValueTranslator = new QPossibleValueTranslator(qInstance, new QSession()); + MemoryRecordStore.setCollectStatistics(true); + possibleValueTranslator.translatePossibleValue(shapeField, 1); + possibleValueTranslator.translatePossibleValue(shapeField, 2); + assertEquals(2, MemoryRecordStore.getStatistics().get(MemoryRecordStore.STAT_QUERIES_RAN), "Should have ran 2 queries so far"); + possibleValueTranslator.translatePossibleValue(shapeField, 2); + possibleValueTranslator.translatePossibleValue(shapeField, 3); + possibleValueTranslator.translatePossibleValue(shapeField, 3); + assertEquals(3, MemoryRecordStore.getStatistics().get(MemoryRecordStore.STAT_QUERIES_RAN), "Should have ran 3 queries in total"); + + /////////////////////////////////////////////////////////////// + // assert that if we prime the cache, we can do just 1 query // + /////////////////////////////////////////////////////////////// + possibleValueTranslator = new QPossibleValueTranslator(qInstance, new QSession()); + List personRecords = List.of( + new QRecord().withTableName(TestUtils.TABLE_NAME_PERSON).withValue("favoriteShapeId", 1), + new QRecord().withTableName(TestUtils.TABLE_NAME_PERSON).withValue("favoriteShapeId", 1), + new QRecord().withTableName(TestUtils.TABLE_NAME_PERSON).withValue("favoriteShapeId", 2), + new QRecord().withTableName(TestUtils.TABLE_NAME_PERSON).withValue("favoriteShapeId", 2), + new QRecord().withTableName(TestUtils.TABLE_NAME_PERSON).withValue("favoriteShapeId", 3) + ); + QTableMetaData personTable = qInstance.getTable(TestUtils.TABLE_NAME_PERSON); + MemoryRecordStore.resetStatistics(); + possibleValueTranslator.primePvsCache(personTable, personRecords); + assertEquals(1, MemoryRecordStore.getStatistics().get(MemoryRecordStore.STAT_QUERIES_RAN), "Should only run 1 query"); + possibleValueTranslator.translatePossibleValue(shapeField, 1); + possibleValueTranslator.translatePossibleValue(shapeField, 2); + assertEquals(1, MemoryRecordStore.getStatistics().get(MemoryRecordStore.STAT_QUERIES_RAN), "Should only run 1 query"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testSetDisplayValuesInRecords() + { + QTableMetaData table = new QTableMetaData() + .withRecordLabelFormat("%s %s") + .withRecordLabelFields("firstName", "lastName") + .withField(new QFieldMetaData("firstName", QFieldType.STRING)) + .withField(new QFieldMetaData("lastName", QFieldType.STRING)) + .withField(new QFieldMetaData("price", QFieldType.DECIMAL).withDisplayFormat(DisplayFormat.CURRENCY)) + .withField(new QFieldMetaData("homeStateId", QFieldType.INTEGER).withPossibleValueSourceName(TestUtils.POSSIBLE_VALUE_SOURCE_STATE)); + + ///////////////////////////////////////////////////////////////// + // first, make sure it doesn't crash with null or empty inputs // + ///////////////////////////////////////////////////////////////// + QPossibleValueTranslator possibleValueTranslator = new QPossibleValueTranslator(TestUtils.defineInstance(), new QSession()); + possibleValueTranslator.translatePossibleValuesInRecords(table, null); + possibleValueTranslator.translatePossibleValuesInRecords(table, Collections.emptyList()); + + List records = List.of( + new QRecord() + .withValue("firstName", "Tim") + .withValue("lastName", "Chamberlain") + .withValue("price", new BigDecimal("3.50")) + .withValue("homeStateId", 1), + new QRecord() + .withValue("firstName", "Tyler") + .withValue("lastName", "Samples") + .withValue("price", new BigDecimal("174999.99")) + .withValue("homeStateId", 2) + ); + + possibleValueTranslator.translatePossibleValuesInRecords(table, records); + + assertNull(records.get(0).getRecordLabel()); // regular display stuff NOT done by PVS translator + assertNull(records.get(0).getDisplayValue("price")); + + assertEquals("IL", records.get(0).getDisplayValue("homeStateId")); + assertEquals("MO", records.get(1).getDisplayValue("homeStateId")); + } + +} diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/values/QValueFormatterTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/values/QValueFormatterTest.java index 4be9cf52..27428209 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/values/QValueFormatterTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/values/QValueFormatterTest.java @@ -30,6 +30,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.DisplayFormat; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.utils.TestUtils; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; @@ -47,24 +48,26 @@ class QValueFormatterTest @Test void testFormatValue() { - assertNull(QValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.COMMAS), null)); + QValueFormatter qValueFormatter = new QValueFormatter(); - assertEquals("1", QValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.COMMAS), 1)); - assertEquals("1,000", QValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.COMMAS), 1000)); - assertEquals("1000", QValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(null), 1000)); - assertEquals("$1,000.00", QValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.CURRENCY), 1000)); - assertEquals("1,000.00", QValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.DECIMAL2_COMMAS), 1000)); - assertEquals("1000.00", QValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.DECIMAL2), 1000)); + assertNull(qValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.COMMAS), null)); - assertEquals("1", QValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.COMMAS), new BigDecimal("1"))); - assertEquals("1,000", QValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.COMMAS), new BigDecimal("1000"))); - assertEquals("1000", QValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.STRING), new BigDecimal("1000"))); - assertEquals("1000", QValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.STRING), 1000)); + assertEquals("1", qValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.COMMAS), 1)); + assertEquals("1,000", qValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.COMMAS), 1000)); + assertEquals("1000", qValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(null), 1000)); + assertEquals("$1,000.00", qValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.CURRENCY), 1000)); + assertEquals("1,000.00", qValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.DECIMAL2_COMMAS), 1000)); + assertEquals("1000.00", qValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.DECIMAL2), 1000)); + + assertEquals("1", qValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.COMMAS), new BigDecimal("1"))); + assertEquals("1,000", qValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.COMMAS), new BigDecimal("1000"))); + assertEquals("1000", qValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.STRING), new BigDecimal("1000"))); + assertEquals("1000", qValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.STRING), 1000)); ////////////////////////////////////////////////// // this one flows through the exceptional cases // ////////////////////////////////////////////////// - assertEquals("1000.01", QValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.COMMAS), new BigDecimal("1000.01"))); + assertEquals("1000.01", qValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.COMMAS), new BigDecimal("1000.01"))); } @@ -75,40 +78,42 @@ class QValueFormatterTest @Test void testFormatRecordLabel() { - QTableMetaData table = new QTableMetaData().withRecordLabelFormat("%s %s").withRecordLabelFields(List.of("firstName", "lastName")); - assertEquals("Darin Kelkhoff", QValueFormatter.formatRecordLabel(table, new QRecord().withValue("firstName", "Darin").withValue("lastName", "Kelkhoff"))); - assertEquals("Darin ", QValueFormatter.formatRecordLabel(table, new QRecord().withValue("firstName", "Darin"))); - assertEquals("Darin ", QValueFormatter.formatRecordLabel(table, new QRecord().withValue("firstName", "Darin").withValue("lastName", null))); + QValueFormatter qValueFormatter = new QValueFormatter(); - table = new QTableMetaData().withRecordLabelFormat("%s " + DisplayFormat.CURRENCY).withRecordLabelFields(List.of("firstName", "price")); - assertEquals("Darin $10,000.00", QValueFormatter.formatRecordLabel(table, new QRecord().withValue("firstName", "Darin").withValue("price", new BigDecimal(10000)))); + QTableMetaData table = new QTableMetaData().withRecordLabelFormat("%s %s").withRecordLabelFields(List.of("firstName", "lastName")); + assertEquals("Darin Kelkhoff", qValueFormatter.formatRecordLabel(table, new QRecord().withValue("firstName", "Darin").withValue("lastName", "Kelkhoff"))); + assertEquals("Darin ", qValueFormatter.formatRecordLabel(table, new QRecord().withValue("firstName", "Darin"))); + assertEquals("Darin ", qValueFormatter.formatRecordLabel(table, new QRecord().withValue("firstName", "Darin").withValue("lastName", null))); + + table = new QTableMetaData().withRecordLabelFormat("%s " + DisplayFormat.CURRENCY).withRecordLabelFields("firstName", "price"); + assertEquals("Darin $10,000.00", qValueFormatter.formatRecordLabel(table, new QRecord().withValue("firstName", "Darin").withValue("price", new BigDecimal(10000)))); table = new QTableMetaData().withRecordLabelFormat(DisplayFormat.DEFAULT).withRecordLabelFields(List.of("id")); - assertEquals("123456", QValueFormatter.formatRecordLabel(table, new QRecord().withValue("id", "123456"))); + assertEquals("123456", qValueFormatter.formatRecordLabel(table, new QRecord().withValue("id", "123456"))); /////////////////////////////////////////////////////// // exceptional flow: no recordLabelFormat specified // /////////////////////////////////////////////////////// table = new QTableMetaData().withPrimaryKeyField("id"); - assertEquals("42", QValueFormatter.formatRecordLabel(table, new QRecord().withValue("id", 42))); + assertEquals("42", qValueFormatter.formatRecordLabel(table, new QRecord().withValue("id", 42))); ///////////////////////////////////////////////// // exceptional flow: no fields for the format // ///////////////////////////////////////////////// table = new QTableMetaData().withRecordLabelFormat("%s %s").withPrimaryKeyField("id"); - assertEquals("128", QValueFormatter.formatRecordLabel(table, new QRecord().withValue("id", 128))); + assertEquals("128", qValueFormatter.formatRecordLabel(table, new QRecord().withValue("id", 128))); ///////////////////////////////////////////////////////// // exceptional flow: not enough fields for the format // ///////////////////////////////////////////////////////// - table = new QTableMetaData().withRecordLabelFormat("%s %s").withRecordLabelFields(List.of("a")).withPrimaryKeyField("id"); - assertEquals("256", QValueFormatter.formatRecordLabel(table, new QRecord().withValue("a", 47).withValue("id", 256))); + table = new QTableMetaData().withRecordLabelFormat("%s %s").withRecordLabelFields("a").withPrimaryKeyField("id"); + assertEquals("256", qValueFormatter.formatRecordLabel(table, new QRecord().withValue("a", 47).withValue("id", 256))); ////////////////////////////////////////////////////////////////////////////////////////////////////////// // exceptional flow (kinda): too many fields for the format (just get the ones that are in the format) // ////////////////////////////////////////////////////////////////////////////////////////////////////////// table = new QTableMetaData().withRecordLabelFormat("%s %s").withRecordLabelFields(List.of("a", "b", "c")).withPrimaryKeyField("id"); - assertEquals("47 48", QValueFormatter.formatRecordLabel(table, new QRecord().withValue("a", 47).withValue("b", 48).withValue("c", 49).withValue("id", 256))); + assertEquals("47 48", qValueFormatter.formatRecordLabel(table, new QRecord().withValue("a", 47).withValue("b", 48).withValue("c", 49).withValue("id", 256))); } @@ -121,40 +126,46 @@ class QValueFormatterTest { QTableMetaData table = new QTableMetaData() .withRecordLabelFormat("%s %s") - .withRecordLabelFields(List.of("firstName", "lastName")) + .withRecordLabelFields("firstName", "lastName") .withField(new QFieldMetaData("firstName", QFieldType.STRING)) .withField(new QFieldMetaData("lastName", QFieldType.STRING)) .withField(new QFieldMetaData("price", QFieldType.DECIMAL).withDisplayFormat(DisplayFormat.CURRENCY)) - .withField(new QFieldMetaData("quantity", QFieldType.INTEGER).withDisplayFormat(DisplayFormat.COMMAS)); + .withField(new QFieldMetaData("quantity", QFieldType.INTEGER).withDisplayFormat(DisplayFormat.COMMAS)) + .withField(new QFieldMetaData("homeStateId", QFieldType.INTEGER).withPossibleValueSourceName(TestUtils.POSSIBLE_VALUE_SOURCE_STATE)); ///////////////////////////////////////////////////////////////// // first, make sure it doesn't crash with null or empty inputs // ///////////////////////////////////////////////////////////////// - QValueFormatter.setDisplayValuesInRecords(table, null); - QValueFormatter.setDisplayValuesInRecords(table, Collections.emptyList()); + QValueFormatter qValueFormatter = new QValueFormatter(); + qValueFormatter.setDisplayValuesInRecords(table, null); + qValueFormatter.setDisplayValuesInRecords(table, Collections.emptyList()); List records = List.of( new QRecord() .withValue("firstName", "Tim") .withValue("lastName", "Chamberlain") .withValue("price", new BigDecimal("3.50")) - .withValue("quantity", 1701), + .withValue("quantity", 1701) + .withValue("homeStateId", 1), new QRecord() .withValue("firstName", "Tyler") .withValue("lastName", "Samples") .withValue("price", new BigDecimal("174999.99")) .withValue("quantity", 47) + .withValue("homeStateId", 2) ); - QValueFormatter.setDisplayValuesInRecords(table, records); + qValueFormatter.setDisplayValuesInRecords(table, records); assertEquals("Tim Chamberlain", records.get(0).getRecordLabel()); assertEquals("$3.50", records.get(0).getDisplayValue("price")); assertEquals("1,701", records.get(0).getDisplayValue("quantity")); + assertEquals("1", records.get(0).getDisplayValue("homeStateId")); // PVS NOT translated by this class. assertEquals("Tyler Samples", records.get(1).getRecordLabel()); assertEquals("$174,999.99", records.get(1).getDisplayValue("price")); assertEquals("47", records.get(1).getDisplayValue("quantity")); + assertEquals("2", records.get(1).getDisplayValue("homeStateId")); // PVS NOT translated by this class. } } \ No newline at end of file diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/adapters/CsvToQRecordAdapterTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/adapters/CsvToQRecordAdapterTest.java index 2593b3e2..a81e5b53 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/adapters/CsvToQRecordAdapterTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/adapters/CsvToQRecordAdapterTest.java @@ -31,6 +31,7 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; @@ -281,4 +282,60 @@ class CsvToQRecordAdapterTest // todo - this is what the method header comment means when it says we don't handle all cases well... // Assertions.assertEquals(List.of("A", "B", "C", "C 2", "C 3"), csvToQRecordAdapter.makeHeadersUnique(List.of("A", "B", "C 2", "C", "C 3"))); } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testByteOrderMarker() + { + CsvToQRecordAdapter csvToQRecordAdapter = new CsvToQRecordAdapter(); + List records = csvToQRecordAdapter.buildRecordsFromCsv(""" + id,firstName + 1,John""", TestUtils.defineTablePerson(), null); + + assertEquals(1, records.get(0).getValueInteger("id")); + assertEquals("John", records.get(0).getValueString("firstName")); + } + + + + /******************************************************************************* + ** Fix an IndexOutOfBounds that we used to throw. + *******************************************************************************/ + @Test + void testTooFewBodyColumns() + { + CsvToQRecordAdapter csvToQRecordAdapter = new CsvToQRecordAdapter(); + List records = csvToQRecordAdapter.buildRecordsFromCsv(""" + id,firstName,lastName + 1,John""", TestUtils.defineTablePerson(), null); + + assertEquals(1, records.get(0).getValueInteger("id")); + assertEquals("John", records.get(0).getValueString("firstName")); + assertNull(records.get(0).getValueString("lastName")); + } + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + public void testTooFewColumnsIndexMapping() + { + int index = 1; + QIndexBasedFieldMapping mapping = new QIndexBasedFieldMapping() + .withMapping("id", index++) + .withMapping("firstName", index++) + .withMapping("lastName", index++); + + CsvToQRecordAdapter csvToQRecordAdapter = new CsvToQRecordAdapter(); + List records = csvToQRecordAdapter.buildRecordsFromCsv("1,John", TestUtils.defineTablePerson(), mapping); + + assertEquals(1, records.get(0).getValueInteger("id")); + assertEquals("John", records.get(0).getValueString("firstName")); + assertNull(records.get(0).getValueString("lastName")); + } + } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricherTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricherTest.java index f9017298..2404c915 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricherTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricherTest.java @@ -22,10 +22,10 @@ package com.kingsrook.qqq.backend.core.instances; +import java.util.ArrayList; import java.util.Collections; -import com.kingsrook.qqq.backend.core.exceptions.QException; -import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.utils.TestUtils; @@ -130,6 +130,20 @@ class QInstanceEnricherTest + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testNameToLabel() + { + assertEquals("Address 2", QInstanceEnricher.nameToLabel("address2")); + assertEquals("Field 20", QInstanceEnricher.nameToLabel("field20")); + assertEquals("Something USA", QInstanceEnricher.nameToLabel("somethingUSA")); + assertEquals("Number 1 Dad", QInstanceEnricher.nameToLabel("number1Dad")); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -146,4 +160,28 @@ class QInstanceEnricherTest assertEquals("tla_and_another_tla", QInstanceEnricher.inferBackendName("TLAAndAnotherTLA")); } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testInferredRecordLabelFormat() + { + QInstance qInstance = TestUtils.defineInstance(); + QTableMetaData table = qInstance.getTable("person").withRecordLabelFormat(null).withRecordLabelFields(new ArrayList<>()); + new QInstanceEnricher().enrich(qInstance); + assertNull(table.getRecordLabelFormat()); + + qInstance = TestUtils.defineInstance(); + table = qInstance.getTable("person").withRecordLabelFormat(null).withRecordLabelFields("firstName"); + new QInstanceEnricher().enrich(qInstance); + assertEquals("%s", table.getRecordLabelFormat()); + + qInstance = TestUtils.defineInstance(); + table = qInstance.getTable("person").withRecordLabelFormat(null).withRecordLabelFields("firstName", "lastName"); + new QInstanceEnricher().enrich(qInstance); + assertEquals("%s %s", table.getRecordLabelFormat()); + } + } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java index a5cefc2c..d4c21bd8 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java @@ -22,16 +22,20 @@ package com.kingsrook.qqq.backend.core.instances; +import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.function.Consumer; import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData; import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon; +import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValue; +import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource; import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier; @@ -110,7 +114,7 @@ class QInstanceValidatorTest @Test public void test_validateNullTables() { - assertValidationFailureReasons((qInstance) -> + assertValidationFailureReasonsAllowingExtraReasons((qInstance) -> { qInstance.setTables(null); qInstance.setProcesses(null); @@ -127,7 +131,7 @@ class QInstanceValidatorTest @Test public void test_validateEmptyTables() { - assertValidationFailureReasons((qInstance) -> + assertValidationFailureReasonsAllowingExtraReasons((qInstance) -> { qInstance.setTables(new HashMap<>()); qInstance.setProcesses(new HashMap<>()); @@ -150,10 +154,13 @@ class QInstanceValidatorTest qInstance.getTable("person").setName("notPerson"); qInstance.getBackend("default").setName("notDefault"); qInstance.getProcess(TestUtils.PROCESS_NAME_GREET_PEOPLE).setName("notGreetPeople"); + qInstance.getPossibleValueSource(TestUtils.POSSIBLE_VALUE_SOURCE_STATE).setName("notStates"); }, "Inconsistent naming for table", "Inconsistent naming for backend", - "Inconsistent naming for process"); + "Inconsistent naming for process", + "Inconsistent naming for possibleValueSource" + ); } @@ -184,6 +191,19 @@ class QInstanceValidatorTest + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + public void test_validateTableBadRecordFormatField() + { + assertValidationFailureReasons((qInstance) -> qInstance.getTable("person").withRecordLabelFields("notAField"), + "not a field"); + } + + + /******************************************************************************* ** Test that if a process specifies a table that doesn't exist, that it fails. ** @@ -252,7 +272,7 @@ class QInstanceValidatorTest @Test public void test_validateFieldWithMissingPossibleValueSource() { - assertValidationFailureReasons((qInstance) -> qInstance.getTable("person").getField("homeState").setPossibleValueSourceName("not a real possible value source"), + assertValidationFailureReasons((qInstance) -> qInstance.getTable("person").getField("homeStateId").setPossibleValueSourceName("not a real possible value source"), "Unrecognized possibleValueSourceName"); } @@ -319,6 +339,7 @@ class QInstanceValidatorTest } + /******************************************************************************* ** *******************************************************************************/ @@ -376,6 +397,7 @@ class QInstanceValidatorTest } + /******************************************************************************* ** *******************************************************************************/ @@ -391,6 +413,7 @@ class QInstanceValidatorTest } + /******************************************************************************* ** *******************************************************************************/ @@ -408,6 +431,96 @@ class QInstanceValidatorTest + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testPossibleValueSourceMissingType() + { + assertValidationFailureReasons((qInstance) -> qInstance.getPossibleValueSource(TestUtils.POSSIBLE_VALUE_SOURCE_STATE).setType(null), + "Missing type for possibleValueSource"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testPossibleValueSourceMissingIdType() + { + assertValidationFailureReasons((qInstance) -> qInstance.getPossibleValueSource(TestUtils.POSSIBLE_VALUE_SOURCE_STATE).setIdType(null), + "Missing an idType for possibleValueSource"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testPossibleValueSourceMisConfiguredEnum() + { + assertValidationFailureReasons((qInstance) -> { + QPossibleValueSource possibleValueSource = qInstance.getPossibleValueSource(TestUtils.POSSIBLE_VALUE_SOURCE_STATE); + possibleValueSource.setTableName("person"); + possibleValueSource.setCustomCodeReference(new QCodeReference()); + possibleValueSource.setEnumValues(null); + }, + "should not have a tableName", + "should not have a customCodeReference", + "is missing enum values"); + + assertValidationFailureReasons((qInstance) -> qInstance.getPossibleValueSource(TestUtils.POSSIBLE_VALUE_SOURCE_STATE).setEnumValues(new ArrayList<>()), + "is missing enum values"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testPossibleValueSourceMisConfiguredTable() + { + assertValidationFailureReasons((qInstance) -> { + QPossibleValueSource possibleValueSource = qInstance.getPossibleValueSource(TestUtils.POSSIBLE_VALUE_SOURCE_SHAPE); + possibleValueSource.setTableName(null); + possibleValueSource.setCustomCodeReference(new QCodeReference()); + possibleValueSource.setEnumValues(List.of(new QPossibleValue<>("test"))); + }, + "should not have enum values", + "should not have a customCodeReference", + "is missing a tableName"); + + assertValidationFailureReasons((qInstance) -> qInstance.getPossibleValueSource(TestUtils.POSSIBLE_VALUE_SOURCE_SHAPE).setTableName("Not a table"), + "Unrecognized table"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testPossibleValueSourceMisConfiguredCustom() + { + assertValidationFailureReasons((qInstance) -> { + QPossibleValueSource possibleValueSource = qInstance.getPossibleValueSource(TestUtils.POSSIBLE_VALUE_SOURCE_CUSTOM); + possibleValueSource.setTableName("person"); + possibleValueSource.setCustomCodeReference(null); + possibleValueSource.setEnumValues(List.of(new QPossibleValue<>("test"))); + }, + "should not have enum values", + "should not have a tableName", + "is missing a customCodeReference"); + + assertValidationFailureReasons((qInstance) -> qInstance.getPossibleValueSource(TestUtils.POSSIBLE_VALUE_SOURCE_CUSTOM).setCustomCodeReference(new QCodeReference()), + "not a possibleValueProvider"); + } + + + /******************************************************************************* ** Run a little setup code on a qInstance; then validate it, and assert that it ** failed validation with reasons that match the supplied vararg-reasons (but allow diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryBackendModuleTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryBackendModuleTest.java index ec1b2673..97ab5288 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryBackendModuleTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryBackendModuleTest.java @@ -48,6 +48,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.session.QSession; import com.kingsrook.qqq.backend.core.utils.TestUtils; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -62,8 +63,9 @@ class MemoryBackendModuleTest /******************************************************************************* ** *******************************************************************************/ + @BeforeEach @AfterEach - void afterEach() + void beforeAndAfter() { MemoryRecordStore.getInstance().reset(); } @@ -120,6 +122,8 @@ class MemoryBackendModuleTest assertEquals(3, new CountAction().execute(countInput).getCount()); + // todo - filters in query + ////////////////// // do an update // ////////////////// diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java index aa537948..915498d7 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java @@ -22,10 +22,12 @@ package com.kingsrook.qqq.backend.core.utils; +import java.io.Serializable; import java.util.List; import com.kingsrook.qqq.backend.core.actions.processes.person.addtopeoplesage.AddAge; import com.kingsrook.qqq.backend.core.actions.processes.person.addtopeoplesage.GetAgeStatistics; import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; +import com.kingsrook.qqq.backend.core.actions.values.QCustomPossibleValueProvider; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput; @@ -39,6 +41,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeUsage; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValue; import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource; import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType; import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData; @@ -80,6 +83,10 @@ public class TestUtils public static final String TABLE_NAME_PERSON_FILE = "personFile"; public static final String TABLE_NAME_ID_AND_NAME_ONLY = "idAndNameOnly"; + public static final String POSSIBLE_VALUE_SOURCE_STATE = "state"; // enum-type + public static final String POSSIBLE_VALUE_SOURCE_SHAPE = "shape"; // table-type + public static final String POSSIBLE_VALUE_SOURCE_CUSTOM = "custom"; // custom-type + /******************************************************************************* @@ -99,6 +106,8 @@ public class TestUtils qInstance.addTable(defineTableShape()); qInstance.addPossibleValueSource(defineStatesPossibleValueSource()); + qInstance.addPossibleValueSource(defineShapePossibleValueSource()); + qInstance.addPossibleValueSource(defineCustomPossibleValueSource()); qInstance.addProcess(defineProcessGreetPeople()); qInstance.addProcess(defineProcessGreetPeopleInteractive()); @@ -141,12 +150,40 @@ public class TestUtils ** Define the "states" possible value source used in standard tests ** *******************************************************************************/ - private static QPossibleValueSource defineStatesPossibleValueSource() + private static QPossibleValueSource defineStatesPossibleValueSource() { - return new QPossibleValueSource() - .withName("state") + return new QPossibleValueSource() + .withName(POSSIBLE_VALUE_SOURCE_STATE) .withType(QPossibleValueSourceType.ENUM) - .withEnumValues(List.of("IL", "MO")); + .withEnumValues(List.of(new QPossibleValue<>(1, "IL"), new QPossibleValue<>(2, "MO"))); + } + + + + /******************************************************************************* + ** Define the "shape" possible value source used in standard tests + ** + *******************************************************************************/ + private static QPossibleValueSource defineShapePossibleValueSource() + { + return new QPossibleValueSource() + .withName(POSSIBLE_VALUE_SOURCE_SHAPE) + .withType(QPossibleValueSourceType.TABLE) + .withTableName(TABLE_NAME_SHAPE); + } + + + + /******************************************************************************* + ** Define the "custom" possible value source used in standard tests + ** + *******************************************************************************/ + private static QPossibleValueSource defineCustomPossibleValueSource() + { + return new QPossibleValueSource() + .withName(POSSIBLE_VALUE_SOURCE_CUSTOM) + .withType(QPossibleValueSourceType.CUSTOM) + .withCustomCodeReference(new QCodeReference(CustomPossibleValueSource.class)); } @@ -205,7 +242,10 @@ public class TestUtils .withField(new QFieldMetaData("lastName", QFieldType.STRING)) .withField(new QFieldMetaData("birthDate", QFieldType.DATE)) .withField(new QFieldMetaData("email", QFieldType.STRING)) - .withField(new QFieldMetaData("homeState", QFieldType.STRING).withPossibleValueSourceName("state")); + .withField(new QFieldMetaData("homeStateId", QFieldType.INTEGER).withPossibleValueSourceName(POSSIBLE_VALUE_SOURCE_STATE)) + .withField(new QFieldMetaData("favoriteShapeId", QFieldType.INTEGER).withPossibleValueSourceName(POSSIBLE_VALUE_SOURCE_SHAPE)) + .withField(new QFieldMetaData("customValue", QFieldType.INTEGER).withPossibleValueSourceName(POSSIBLE_VALUE_SOURCE_CUSTOM)) + ; } @@ -219,6 +259,7 @@ public class TestUtils .withName(TABLE_NAME_SHAPE) .withBackendName(MEMORY_BACKEND_NAME) .withPrimaryKeyField("id") + .withRecordLabelFields("name") .withField(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false)) .withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME).withIsEditable(false)) .withField(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME).withIsEditable(false)) @@ -452,4 +493,21 @@ public class TestUtils """); } + + + /******************************************************************************* + ** + *******************************************************************************/ + public static class CustomPossibleValueSource implements QCustomPossibleValueProvider + { + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public QPossibleValue getPossibleValue(Serializable idValue) + { + return (new QPossibleValue<>(idValue, "Custom[" + idValue + "]")); + } + } } diff --git a/qqq-backend-core/src/test/resources/personQInstance.json b/qqq-backend-core/src/test/resources/personQInstance.json index 9b1d333b..a0d2f454 100644 --- a/qqq-backend-core/src/test/resources/personQInstance.json +++ b/qqq-backend-core/src/test/resources/personQInstance.json @@ -60,8 +60,8 @@ "type": "STRING", "possibleValueSourceName": null }, - "homeState": { - "name": "homeState", + "homeStateId": { + "name": "homeStateId", "label": null, "backendName": null, "type": "STRING", diff --git a/qqq-backend-core/src/test/resources/personQInstanceIncludingBackend.json b/qqq-backend-core/src/test/resources/personQInstanceIncludingBackend.json index 5502e715..bda0d747 100644 --- a/qqq-backend-core/src/test/resources/personQInstanceIncludingBackend.json +++ b/qqq-backend-core/src/test/resources/personQInstanceIncludingBackend.json @@ -27,8 +27,8 @@ "type": "DATE_TIME", "possibleValueSourceName": null }, - "homeState": { - "name": "homeState", + "homeStateId": { + "name": "homeStateId", "backendName": null, "label": null, "type": "STRING", diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryAction.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryAction.java index 02718222..566a220e 100644 --- a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryAction.java +++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryAction.java @@ -200,6 +200,10 @@ public class RDBMSQueryAction extends AbstractRDBMSAction implements QueryInterf { return (QueryManager.getLocalDateTime(resultSet, i)); } + case BOOLEAN: + { + return (QueryManager.getBoolean(resultSet, i)); + } default: { throw new IllegalStateException("Unexpected field type: " + qFieldMetaData.getType()); diff --git a/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryActionTest.java b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryActionTest.java index 3e4c37ba..d440dcc7 100644 --- a/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryActionTest.java +++ b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryActionTest.java @@ -428,12 +428,14 @@ public class RDBMSQueryActionTest extends RDBMSActionTest /******************************************************************************* ** This doesn't really test any RDBMS code, but is a checkpoint that the core - ** module is populating displayValues when it performs the system-level query action. + ** module is populating displayValues when it performs the system-level query action + ** (if so requested by input field). *******************************************************************************/ @Test public void testThatDisplayValuesGetSetGoingThroughQueryAction() throws QException { QueryInput queryInput = initQueryRequest(); + queryInput.setShouldGenerateDisplayValues(true); QueryOutput queryOutput = new QueryAction().execute(queryInput); Assertions.assertEquals(5, queryOutput.getRecords().size(), "Unfiltered query should find all rows"); diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java index 3e1fcf77..72e0ee79 100644 --- a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java @@ -34,6 +34,7 @@ import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.function.Supplier; import com.kingsrook.qqq.backend.core.actions.async.AsyncJobManager; import com.kingsrook.qqq.backend.core.actions.metadata.MetaDataAction; import com.kingsrook.qqq.backend.core.actions.metadata.ProcessMetaDataAction; @@ -109,11 +110,16 @@ public class QJavalinImplementation { private static final Logger LOG = LogManager.getLogger(QJavalinImplementation.class); - private static final int SESSION_COOKIE_AGE = 60 * 60 * 24; + private static final int SESSION_COOKIE_AGE = 60 * 60 * 24; private static final String SESSION_ID_COOKIE_NAME = "sessionId"; static QInstance qInstance; + private static Supplier qInstanceHotSwapSupplier; + private static long lastQInstanceHotSwapMillis; + + private static final long MILLIS_BETWEEN_HOT_SWAPS = 2500; + private static int DEFAULT_PORT = 8001; private static Javalin service; @@ -166,6 +172,44 @@ public class QJavalinImplementation // todo base path from arg? - and then potentially multiple instances too (chosen based on the root path??) service = Javalin.create().start(port); service.routes(getRoutes()); + service.before(QJavalinImplementation::hotSwapQInstance); + } + + + + /******************************************************************************* + ** If there's a qInstanceHotSwapSupplier, and its been a little while, replace + ** the qInstance with a new one from the supplier. Meant to be used while doing + ** development. + *******************************************************************************/ + public static void hotSwapQInstance(Context context) + { + if(qInstanceHotSwapSupplier != null) + { + long now = System.currentTimeMillis(); + if(now - lastQInstanceHotSwapMillis < MILLIS_BETWEEN_HOT_SWAPS) + { + return; + } + + lastQInstanceHotSwapMillis = now; + + try + { + QInstance newQInstance = qInstanceHotSwapSupplier.get(); + new QInstanceValidator().validate(newQInstance); + QJavalinImplementation.qInstance = newQInstance; + LOG.info("Swapped qInstance"); + } + catch(QInstanceValidationException e) + { + LOG.warn(e.getMessage()); + } + catch(Exception e) + { + LOG.error("Error swapping QInstance", e); + } + } } @@ -249,7 +293,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 +310,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 +353,7 @@ public class QJavalinImplementation { try { - String table = context.pathParam("table"); + String table = context.pathParam("table"); List primaryKeys = new ArrayList<>(); primaryKeys.add(context.pathParam("primaryKey")); @@ -338,9 +382,9 @@ public class QJavalinImplementation { try { - String table = context.pathParam("table"); + String table = context.pathParam("table"); List recordList = new ArrayList<>(); - QRecord record = new QRecord(); + QRecord record = new QRecord(); record.setTableName(table); recordList.add(record); @@ -382,9 +426,9 @@ public class QJavalinImplementation { try { - String table = context.pathParam("table"); + String table = context.pathParam("table"); List recordList = new ArrayList<>(); - QRecord record = new QRecord(); + QRecord record = new QRecord(); record.setTableName(table); recordList.add(record); @@ -429,6 +473,8 @@ public class QJavalinImplementation setupSession(context, queryInput); queryInput.setTableName(tableName); + queryInput.setShouldGenerateDisplayValues(true); + queryInput.setShouldTranslatePossibleValues(true); // todo - validate that the primary key is of the proper type (e.g,. not a string for an id field) // and throw a 400-series error (tell the user bad-request), rather than, we're doing a 500 (server error) @@ -524,6 +570,8 @@ public class QJavalinImplementation QueryInput queryInput = new QueryInput(qInstance); setupSession(context, queryInput); queryInput.setTableName(context.pathParam("table")); + queryInput.setShouldGenerateDisplayValues(true); + queryInput.setShouldTranslatePossibleValues(true); queryInput.setSkip(integerQueryParam(context, "skip")); queryInput.setLimit(integerQueryParam(context, "limit")); @@ -836,4 +884,14 @@ public class QJavalinImplementation return (null); } + + + /******************************************************************************* + ** Setter for qInstanceHotSwapSupplier + *******************************************************************************/ + public static void setQInstanceHotSwapSupplier(Supplier qInstanceHotSwapSupplier) + { + QJavalinImplementation.qInstanceHotSwapSupplier = qInstanceHotSwapSupplier; + } + } diff --git a/qqq-middleware-picocli/src/main/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementation.java b/qqq-middleware-picocli/src/main/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementation.java index e83fe394..a59cada8 100644 --- a/qqq-middleware-picocli/src/main/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementation.java +++ b/qqq-middleware-picocli/src/main/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementation.java @@ -536,6 +536,11 @@ public class QPicoCliImplementation queryInput.setSession(session); queryInput.setTableName(tableName); queryInput.setSkip(subParseResult.matchedOptionValue("skip", null)); + + // todo - think about these (e.g., based on user's requested output format? + // queryInput.setShouldGenerateDisplayValues(true); + // queryInput.setShouldTranslatePossibleValues(true); + String primaryKeyValue = subParseResult.matchedPositionalValue(0, null); if(primaryKeyValue == null) @@ -581,6 +586,10 @@ public class QPicoCliImplementation queryInput.setLimit(subParseResult.matchedOptionValue("limit", null)); queryInput.setFilter(generateQueryFilter(subParseResult)); + // todo - think about these (e.g., based on user's requested output format? + // queryInput.setShouldGenerateDisplayValues(true); + // queryInput.setShouldTranslatePossibleValues(true); + QueryAction queryAction = new QueryAction(); QueryOutput queryOutput = queryAction.execute(queryInput); commandLine.getOut().println(JsonUtils.toPrettyJson(queryOutput)); diff --git a/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleMetaDataProvider.java b/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleMetaDataProvider.java index 9be5c4d6..7c0e5c00 100644 --- a/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleMetaDataProvider.java +++ b/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleMetaDataProvider.java @@ -253,7 +253,7 @@ public class SampleMetaDataProvider .withBackendName(RDBMS_BACKEND_NAME) .withPrimaryKeyField("id") .withRecordLabelFormat("%s %s") - .withRecordLabelFields(List.of("firstName", "lastName")) + .withRecordLabelFields("firstName", "lastName") .withField(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false)) .withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME).withBackendName("create_date").withIsEditable(false)) .withField(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME).withBackendName("modify_date").withIsEditable(false)) @@ -267,7 +267,7 @@ public class SampleMetaDataProvider .withSection(new QFieldSection("identity", "Identity", new QIcon("badge"), Tier.T1, List.of("id", "firstName", "lastName"))) .withSection(new QFieldSection("basicInfo", "Basic Info", new QIcon("dataset"), Tier.T2, List.of("email", "birthDate"))) - .withSection(new QFieldSection("employmentInfo", "Employment Info", new QIcon("work"), Tier.T2, List.of("annualSalary", "daysWorked"))) + .withSection(new QFieldSection("employmentInfo", "Employment Info", new QIcon("work"), Tier.T2, List.of("isEmployed", "annualSalary", "daysWorked"))) .withSection(new QFieldSection("dates", "Dates", new QIcon("calendar_month"), Tier.T3, List.of("createDate", "modifyDate"))); QInstanceEnricher.setInferredFieldBackendNames(qTableMetaData); diff --git a/qqq-sample-project/src/test/resources/prime-test-database.sql b/qqq-sample-project/src/test/resources/prime-test-database.sql index ef295c31..10185156 100644 --- a/qqq-sample-project/src/test/resources/prime-test-database.sql +++ b/qqq-sample-project/src/test/resources/prime-test-database.sql @@ -31,15 +31,16 @@ CREATE TABLE person birth_date DATE, email VARCHAR(250) NOT NULL, + is_employed BOOLEAN, annual_salary DECIMAL(12, 2), days_worked INTEGER ); -INSERT INTO person (id, first_name, last_name, birth_date, email, annual_salary, days_worked) VALUES (1, 'Darin', 'Kelkhoff', '1980-05-31', 'darin.kelkhoff@gmail.com', 75003.50, 1001); -INSERT INTO person (id, first_name, last_name, birth_date, email, annual_salary, days_worked) VALUES (2, 'James', 'Maes', '1980-05-15', 'jmaes@mmltholdings.com', 150000, 10100); -INSERT INTO person (id, first_name, last_name, birth_date, email, annual_salary, days_worked) VALUES (3, 'Tim', 'Chamberlain', '1976-05-28', 'tchamberlain@mmltholdings.com', 300000, 100100); -INSERT INTO person (id, first_name, last_name, birth_date, email, annual_salary, days_worked) VALUES (4, 'Tyler', 'Samples', NULL, 'tsamples@mmltholdings.com', 950000, 75); -INSERT INTO person (id, first_name, last_name, birth_date, email, annual_salary, days_worked) VALUES (5, 'Garret', 'Richardson', '1981-01-01', 'grichardson@mmltholdings.com', 1500000, 1); +INSERT INTO person (id, first_name, last_name, birth_date, email, is_employed, annual_salary, days_worked) VALUES (1, 'Darin', 'Kelkhoff', '1980-05-31', 'darin.kelkhoff@gmail.com', 1, 75003.50, 1001); +INSERT INTO person (id, first_name, last_name, birth_date, email, is_employed, annual_salary, days_worked) VALUES (2, 'James', 'Maes', '1980-05-15', 'jmaes@mmltholdings.com', 1, 150000, 10100); +INSERT INTO person (id, first_name, last_name, birth_date, email, is_employed, annual_salary, days_worked) VALUES (3, 'Tim', 'Chamberlain', '1976-05-28', 'tchamberlain@mmltholdings.com', 1, 300000, 100100); +INSERT INTO person (id, first_name, last_name, birth_date, email, is_employed, annual_salary, days_worked) VALUES (4, 'Tyler', 'Samples', NULL, 'tsamples@mmltholdings.com', 1, 950000, 75); +INSERT INTO person (id, first_name, last_name, birth_date, email, is_employed, annual_salary, days_worked) VALUES (5, 'Garret', 'Richardson', '1981-01-01', 'grichardson@mmltholdings.com', 0, 1500000, 1); DROP TABLE IF EXISTS carrier; CREATE TABLE carrier