diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/MetaDataAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/MetaDataAction.java index 4f933dd8..f15e319c 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/MetaDataAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/MetaDataAction.java @@ -22,14 +22,19 @@ package com.kingsrook.qqq.backend.core.actions.metadata; +import java.util.ArrayList; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import com.kingsrook.qqq.backend.core.actions.ActionHelper; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataInput; import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataOutput; +import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendAppMetaData; import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendProcessMetaData; import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendTableMetaData; +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.QProcessMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; @@ -50,22 +55,87 @@ public class MetaDataAction // todo pre-customization - just get to modify the request? MetaDataOutput metaDataOutput = new MetaDataOutput(); + Map treeNodes = new LinkedHashMap<>(); + + ///////////////////////////////////// + // map tables to frontend metadata // + ///////////////////////////////////// Map tables = new LinkedHashMap<>(); for(Map.Entry entry : metaDataInput.getInstance().getTables().entrySet()) { tables.put(entry.getKey(), new QFrontendTableMetaData(entry.getValue(), false)); + treeNodes.put(entry.getKey(), new QFrontendAppMetaData(entry.getValue())); } metaDataOutput.setTables(tables); + //////////////////////////////////////// + // map processes to frontend metadata // + //////////////////////////////////////// Map processes = new LinkedHashMap<>(); for(Map.Entry entry : metaDataInput.getInstance().getProcesses().entrySet()) { processes.put(entry.getKey(), new QFrontendProcessMetaData(entry.getValue(), false)); + treeNodes.put(entry.getKey(), new QFrontendAppMetaData(entry.getValue())); } metaDataOutput.setProcesses(processes); - // todo post-customization - can do whatever w/ the result if you want + /////////////////////////////////// + // map apps to frontend metadata // + /////////////////////////////////// + Map apps = new LinkedHashMap<>(); + for(Map.Entry entry : metaDataInput.getInstance().getApps().entrySet()) + { + apps.put(entry.getKey(), new QFrontendAppMetaData(entry.getValue())); + treeNodes.put(entry.getKey(), new QFrontendAppMetaData(entry.getValue())); + + for(QAppChildMetaData child : entry.getValue().getChildren()) + { + apps.get(entry.getKey()).addChild(new QFrontendAppMetaData(child)); + } + } + metaDataOutput.setApps(apps); + + //////////////////////////////////////////////// + // organize app tree nodes by their hierarchy // + //////////////////////////////////////////////// + List appTree = new ArrayList<>(); + for(QAppMetaData appMetaData : metaDataInput.getInstance().getApps().values()) + { + if(appMetaData.getParentAppName() == null) + { + buildAppTree(treeNodes, appTree, appMetaData); + } + } + metaDataOutput.setAppTree(appTree); + + // todo post-customization - can do whatever w/ the result if you want? return metaDataOutput; } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private void buildAppTree(Map treeNodes, List nodeList, QAppChildMetaData childMetaData) + { + QFrontendAppMetaData treeNode = treeNodes.get(childMetaData.getName()); + if(treeNode == null) + { + return; + } + + nodeList.add(treeNode); + if(childMetaData instanceof QAppMetaData app) + { + if(app.getChildren() != null) + { + for(QAppChildMetaData child : app.getChildren()) + { + buildAppTree(treeNodes, treeNode.getChildren(), child); + } + } + } + } } 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 new file mode 100644 index 00000000..107b66bd --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QValueFormatter.java @@ -0,0 +1,75 @@ +/* + * 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.fields.QFieldMetaData; +import com.kingsrook.qqq.backend.core.utils.StringUtils; +import com.kingsrook.qqq.backend.core.utils.ValueUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + + +/******************************************************************************* + ** Utility to apply display formats to values for fields + *******************************************************************************/ +public class QValueFormatter +{ + private static final Logger LOG = LogManager.getLogger(QValueFormatter.class); + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static String formatValue(QFieldMetaData field, Serializable value) + { + ////////////////////////////////// + // null values get null results // + ////////////////////////////////// + if(value == null) + { + return (null); + } + + //////////////////////////////////////////////////////// + // if the field has a display format, try to apply it // + //////////////////////////////////////////////////////// + if(StringUtils.hasContent(field.getDisplayFormat())) + { + try + { + return (field.getDisplayFormat().formatted(value)); + } + catch(Exception e) + { + LOG.warn("Error formatting value [" + value + "] for field [" + field.getName() + "] with format [" + field.getDisplayFormat() + "]: " + e.getMessage()); + } + } + + //////////////////////////////////////// + // by default, just get back a string // + //////////////////////////////////////// + return (ValueUtils.getValueAsString(value)); + } +} 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 b69a1da9..d1901d1a 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 @@ -31,6 +31,7 @@ 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.processes.QBackendStepMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QComponentType; import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendComponentMetaData; @@ -84,6 +85,11 @@ public class QInstanceEnricher { qInstance.getBackends().values().forEach(this::enrich); } + + if(qInstance.getApps() != null) + { + qInstance.getApps().values().forEach(this::enrich); + } } @@ -179,6 +185,19 @@ public class QInstanceEnricher + /******************************************************************************* + ** + *******************************************************************************/ + private void enrich(QAppMetaData app) + { + if(!StringUtils.hasContent(app.getLabel())) + { + app.setLabel(nameToLabel(app.getName())); + } + } + + + /******************************************************************************* ** *******************************************************************************/ 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 f1e018ff..c7665022 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 @@ -23,10 +23,15 @@ package com.kingsrook.qqq.backend.core.instances; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; 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.layout.QAppChildMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils; @@ -76,60 +81,10 @@ public class QInstanceValidator List errors = new ArrayList<>(); try { - if(assertCondition(errors, CollectionUtils.nullSafeHasContents(qInstance.getBackends()), - "At least 1 backend must be defined.")) - { - qInstance.getBackends().forEach((backendName, backend) -> - { - assertCondition(errors, Objects.equals(backendName, backend.getName()), - "Inconsistent naming for backend: " + backendName + "/" + backend.getName() + "."); - }); - } - - ///////////////////////// - // validate the tables // - ///////////////////////// - if(assertCondition(errors, CollectionUtils.nullSafeHasContents(qInstance.getTables()), - "At least 1 table must be defined.")) - { - qInstance.getTables().forEach((tableName, table) -> - { - assertCondition(errors, Objects.equals(tableName, table.getName()), - "Inconsistent naming for table: " + tableName + "/" + table.getName() + "."); - - //////////////////////////////////////// - // validate the backend for the table // - //////////////////////////////////////// - if(assertCondition(errors, StringUtils.hasContent(table.getBackendName()), - "Missing backend name for table " + tableName + ".")) - { - if(CollectionUtils.nullSafeHasContents(qInstance.getBackends())) - { - assertCondition(errors, qInstance.getBackendForTable(tableName) != null, - "Unrecognized backend " + table.getBackendName() + " for table " + tableName + "."); - } - } - - ////////////////////////////////// - // validate fields in the table // - ////////////////////////////////// - if(assertCondition(errors, CollectionUtils.nullSafeHasContents(table.getFields()), - "At least 1 field must be defined in table " + tableName + ".")) - { - table.getFields().forEach((fieldName, field) -> - { - assertCondition(errors, Objects.equals(fieldName, field.getName()), - "Inconsistent naming in table " + tableName + " for field " + fieldName + "/" + field.getName() + "."); - - if(field.getPossibleValueSourceName() != null) - { - assertCondition(errors, qInstance.getPossibleValueSource(field.getPossibleValueSourceName()) != null, - "Unrecognized possibleValueSourceName " + field.getPossibleValueSourceName() + " in table " + tableName + " for field " + fieldName + "."); - } - }); - } - }); - } + validateBackends(qInstance, errors); + validateTables(qInstance, errors); + validateProcesses(qInstance, errors); + validateApps(qInstance, errors); } catch(Exception e) { @@ -149,6 +104,181 @@ public class QInstanceValidator /******************************************************************************* ** *******************************************************************************/ + private void validateBackends(QInstance qInstance, List errors) + { + if(assertCondition(errors, CollectionUtils.nullSafeHasContents(qInstance.getBackends()), "At least 1 backend must be defined.")) + { + qInstance.getBackends().forEach((backendName, backend) -> + { + assertCondition(errors, Objects.equals(backendName, backend.getName()), "Inconsistent naming for backend: " + backendName + "/" + backend.getName() + "."); + }); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private void validateTables(QInstance qInstance, List errors) + { + if(assertCondition(errors, CollectionUtils.nullSafeHasContents(qInstance.getTables()), + "At least 1 table must be defined.")) + { + qInstance.getTables().forEach((tableName, table) -> + { + assertCondition(errors, Objects.equals(tableName, table.getName()), "Inconsistent naming for table: " + tableName + "/" + table.getName() + "."); + + validateAppChildHasValidParentAppName(qInstance, errors, table); + + //////////////////////////////////////// + // validate the backend for the table // + //////////////////////////////////////// + if(assertCondition(errors, StringUtils.hasContent(table.getBackendName()), + "Missing backend name for table " + tableName + ".")) + { + if(CollectionUtils.nullSafeHasContents(qInstance.getBackends())) + { + assertCondition(errors, qInstance.getBackendForTable(tableName) != null, "Unrecognized backend " + table.getBackendName() + " for table " + tableName + "."); + } + } + + ////////////////////////////////// + // validate fields in the table // + ////////////////////////////////// + if(assertCondition(errors, CollectionUtils.nullSafeHasContents(table.getFields()), "At least 1 field must be defined in table " + tableName + ".")) + { + table.getFields().forEach((fieldName, field) -> + { + assertCondition(errors, Objects.equals(fieldName, field.getName()), + "Inconsistent naming in table " + tableName + " for field " + fieldName + "/" + field.getName() + "."); + + if(field.getPossibleValueSourceName() != null) + { + assertCondition(errors, qInstance.getPossibleValueSource(field.getPossibleValueSourceName()) != null, + "Unrecognized possibleValueSourceName " + field.getPossibleValueSourceName() + " in table " + tableName + " for field " + fieldName + "."); + } + }); + } + }); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private void validateProcesses(QInstance qInstance, List errors) + { + if(!CollectionUtils.nullSafeIsEmpty(qInstance.getProcesses())) + { + qInstance.getProcesses().forEach((processName, process) -> + { + assertCondition(errors, Objects.equals(processName, process.getName()), "Inconsistent naming for process: " + processName + "/" + process.getName() + "."); + + validateAppChildHasValidParentAppName(qInstance, errors, process); + + ///////////////////////////////////////////// + // validate the table name for the process // + ///////////////////////////////////////////// + if(process.getTableName() != null) + { + assertCondition(errors, qInstance.getTable(process.getTableName()) != null, "Unrecognized table " + process.getTableName() + " for process " + processName + "."); + } + + /////////////////////////////////// + // validate steps in the process // + /////////////////////////////////// + if(assertCondition(errors, CollectionUtils.nullSafeHasContents(process.getStepList()), "At least 1 step must be defined in process " + processName + ".")) + { + int index = 0; + for(QStepMetaData step : process.getStepList()) + { + assertCondition(errors, StringUtils.hasContent(step.getName()), "Missing name for a step at index " + index + " in process " + processName); + index++; + } + } + }); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private void validateApps(QInstance qInstance, List errors) + { + if(!CollectionUtils.nullSafeIsEmpty(qInstance.getApps())) + { + qInstance.getApps().forEach((appName, app) -> + { + assertCondition(errors, Objects.equals(appName, app.getName()), "Inconsistent naming for app: " + appName + "/" + app.getName() + "."); + + validateAppChildHasValidParentAppName(qInstance, errors, app); + + Set appsVisited = new HashSet<>(); + visitAppCheckingForCycles(app, appsVisited, errors); + + if(app.getChildren() != null) + { + Set childNames = new HashSet<>(); + for(QAppChildMetaData child : app.getChildren()) + { + assertCondition(errors, Objects.equals(appName, child.getParentAppName()), "Child " + child.getName() + " of app " + appName + " does not have its parent app properly set."); + assertCondition(errors, !childNames.contains(child.getName()), "App " + appName + " contains more than one child named " + child.getName()); + childNames.add(child.getName()); + } + } + }); + } + } + + + + /******************************************************************************* + ** Check if an app's child list can recursively be traversed without finding a + ** duplicate, which would indicate a cycle (e.g., an error) + *******************************************************************************/ + private void visitAppCheckingForCycles(QAppMetaData app, Set appsVisited, List errors) + { + if(assertCondition(errors, !appsVisited.contains(app.getName()), "Circular app reference detected, involving " + app.getName())) + { + appsVisited.add(app.getName()); + if(app.getChildren() != null) + { + for(QAppChildMetaData child : app.getChildren()) + { + if(child instanceof QAppMetaData childApp) + { + visitAppCheckingForCycles(childApp, appsVisited, errors); + } + } + } + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private void validateAppChildHasValidParentAppName(QInstance qInstance, List errors, QAppChildMetaData appChild) + { + if(appChild.getParentAppName() != null) + { + assertCondition(errors, qInstance.getApp(appChild.getParentAppName()) != null, "Unrecognized parent app " + appChild.getParentAppName() + " for " + appChild.getName() + "."); + } + } + + + + /******************************************************************************* + ** For the given input condition, if it's true, then we're all good (and return true). + ** But if it's false, add the provided message to the list of errors (and return false, + ** e.g., in case you need to stop evaluating rules to avoid exceptions). + *******************************************************************************/ private boolean assertCondition(List errors, boolean condition, String message) { if(!condition) diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/AbstractActionInput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/AbstractActionInput.java index 552cba50..db855917 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/AbstractActionInput.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/AbstractActionInput.java @@ -87,7 +87,7 @@ public abstract class AbstractActionInput catch(QInstanceValidationException e) { LOG.warn(e); - throw (new IllegalArgumentException("QInstance failed validation" + e.getMessage())); + throw (new IllegalArgumentException("QInstance failed validation" + e.getMessage(), e)); } } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/metadata/MetaDataOutput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/metadata/MetaDataOutput.java index 2685bcfa..f47cc424 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/metadata/MetaDataOutput.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/metadata/MetaDataOutput.java @@ -22,8 +22,10 @@ package com.kingsrook.qqq.backend.core.model.actions.metadata; +import java.util.List; import java.util.Map; import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput; +import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendAppMetaData; import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendProcessMetaData; import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendTableMetaData; @@ -36,6 +38,9 @@ public class MetaDataOutput extends AbstractActionOutput { private Map tables; private Map processes; + private Map apps; + + private List appTree; @@ -80,4 +85,49 @@ public class MetaDataOutput extends AbstractActionOutput { this.processes = processes; } + + + + + /******************************************************************************* + ** Getter for appTree + ** + *******************************************************************************/ + public List getAppTree() + { + return appTree; + } + + + + /******************************************************************************* + ** Setter for appTree + ** + *******************************************************************************/ + public void setAppTree(List appTree) + { + this.appTree = appTree; + } + + + + /******************************************************************************* + ** Getter for apps + ** + *******************************************************************************/ + public Map getApps() + { + return apps; + } + + + + /******************************************************************************* + ** Setter for apps + ** + *******************************************************************************/ + public void setApps(Map apps) + { + this.apps = apps; + } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecord.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecord.java index 3f3a7f1f..2ef044ff 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecord.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecord.java @@ -29,7 +29,9 @@ import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter; 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.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.utils.ValueUtils; @@ -69,6 +71,7 @@ public class QRecord implements Serializable } + /******************************************************************************* ** *******************************************************************************/ @@ -105,6 +108,16 @@ public class QRecord implements Serializable + /******************************************************************************* + ** + *******************************************************************************/ + public void setValue(QFieldMetaData field, Serializable value) + { + values.put(field.getName(), value); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -126,6 +139,16 @@ public class QRecord implements Serializable + /******************************************************************************* + ** + *******************************************************************************/ + public void setDisplayValue(QFieldMetaData field, Serializable rawValue) + { + displayValues.put(field.getName(), QValueFormatter.formatValue(field, rawValue)); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -137,6 +160,17 @@ public class QRecord implements Serializable + /******************************************************************************* + ** + *******************************************************************************/ + public QRecord withDisplayValue(QFieldMetaData field, Serializable rawValue) + { + setDisplayValue(field, rawValue); + return (this); + } + + + /******************************************************************************* ** Getter for tableName ** @@ -355,6 +389,7 @@ public class QRecord implements Serializable } + /******************************************************************************* ** Getter for errors ** @@ -399,6 +434,7 @@ public class QRecord implements Serializable } + /******************************************************************************* ** Convert this record to an QRecordEntity *******************************************************************************/ 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 620f9f65..6f4c5230 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 @@ -24,10 +24,12 @@ package com.kingsrook.qqq.backend.core.model.metadata; import java.util.ArrayList; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import com.fasterxml.jackson.annotation.JsonIgnore; import com.kingsrook.qqq.backend.core.instances.QInstanceValidationKey; +import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData; import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource; import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData; @@ -49,9 +51,13 @@ public class QInstance private QAuthenticationMetaData authentication = null; - private Map tables = new HashMap<>(); - private Map> possibleValueSources = new HashMap<>(); - private Map processes = new HashMap<>(); + //////////////////////////////////////////////////////////////////////////////////////////// + // 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<>(); // todo - lock down the object (no more changes allowed) after it's been validated? @@ -171,6 +177,11 @@ public class QInstance *******************************************************************************/ public QTableMetaData getTable(String name) { + if(this.tables == null) + { + return (null); + } + return (this.tables.get(name)); } @@ -260,6 +271,40 @@ public class QInstance + /******************************************************************************* + ** + *******************************************************************************/ + public void addApp(QAppMetaData app) + { + this.addApp(app.getName(), app); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public void addApp(String name, QAppMetaData app) + { + if(this.apps.containsKey(name)) + { + throw (new IllegalArgumentException("Attempted to add a second app with name: " + name)); + } + this.apps.put(name, app); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public QAppMetaData getApp(String name) + { + return (this.apps.get(name)); + } + + + /******************************************************************************* ** Getter for backends ** @@ -348,6 +393,28 @@ public class QInstance + /******************************************************************************* + ** Getter for apps + ** + *******************************************************************************/ + public Map getApps() + { + return apps; + } + + + + /******************************************************************************* + ** Setter for apps + ** + *******************************************************************************/ + public void setApps(Map apps) + { + this.apps = apps; + } + + + /******************************************************************************* ** Getter for hasBeenValidated ** diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/DisplayFormat.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/DisplayFormat.java new file mode 100644 index 00000000..4c884ed0 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/DisplayFormat.java @@ -0,0 +1,40 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.model.metadata.fields; + + +/******************************************************************************* + ** + *******************************************************************************/ +public interface DisplayFormat +{ + String DEFAULT = "%s"; + String STRING = "%s"; + String COMMAS = "%,d"; + String DECIMAL1_COMMAS = "%,.1f"; + String DECIMAL2_COMMAS = "%,.2f"; + String DECIMAL3_COMMAS = "%,.3f"; + String DECIMAL1 = "%.1f"; + String DECIMAL2 = "%.2f"; + String DECIMAL3 = "%.3f"; + String CURRENCY = "$%,.2f"; +} 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 99c0fbf3..066d2aca 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 @@ -51,6 +51,7 @@ public class QFieldMetaData // propose doing that in a secondary field, e.g., "onlyEditableOn=insert|update" // /////////////////////////////////////////////////////////////////////////////////// + private String displayFormat = "%s"; private Serializable defaultValue; private String possibleValueSourceName; @@ -395,4 +396,36 @@ public class QFieldMetaData return (this); } + + /******************************************************************************* + ** Getter for displayFormat + ** + *******************************************************************************/ + public String getDisplayFormat() + { + return displayFormat; + } + + + + /******************************************************************************* + ** Setter for displayFormat + ** + *******************************************************************************/ + public void setDisplayFormat(String displayFormat) + { + this.displayFormat = displayFormat; + } + + + /******************************************************************************* + ** Fluent setter for displayFormat + ** + *******************************************************************************/ + public QFieldMetaData withDisplayFormat(String displayFormat) + { + this.displayFormat = displayFormat; + return (this); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldType.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldType.java index 41ffdcc4..d0c9c357 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldType.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldType.java @@ -65,6 +65,10 @@ public enum QFieldType { return (INTEGER); } + if(c.equals(Boolean.class)) + { + return (BOOLEAN); + } if(c.equals(BigDecimal.class)) { return (DECIMAL); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/AppTreeNodeType.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/AppTreeNodeType.java new file mode 100644 index 00000000..09c954f4 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/AppTreeNodeType.java @@ -0,0 +1,33 @@ +/* + * 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.frontend; + + +/******************************************************************************* + ** + *******************************************************************************/ +public enum AppTreeNodeType +{ + TABLE, + PROCESS, + APP +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendAppMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendAppMetaData.java new file mode 100644 index 00000000..85d9d885 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendAppMetaData.java @@ -0,0 +1,164 @@ +/* + * 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.frontend; + + +import java.util.ArrayList; +import java.util.List; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +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.QProcessMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.utils.StringUtils; + + +/******************************************************************************* + * Version of QAppMetaData that's meant for transmitting to a frontend. + * + *******************************************************************************/ +@JsonInclude(Include.NON_NULL) +public class QFrontendAppMetaData +{ + private AppTreeNodeType type; + + private String name; + private String label; + + private List children = new ArrayList<>(); + + private String iconName; + + + + /******************************************************************************* + ** + *******************************************************************************/ + public QFrontendAppMetaData(QAppChildMetaData appChildMetaData) + { + this.name = appChildMetaData.getName(); + this.label = appChildMetaData.getLabel(); + + if(appChildMetaData.getClass().equals(QTableMetaData.class)) + { + this.type = AppTreeNodeType.TABLE; + } + else if(appChildMetaData.getClass().equals(QProcessMetaData.class)) + { + this.type = AppTreeNodeType.PROCESS; + } + else if(appChildMetaData.getClass().equals(QAppMetaData.class)) + { + this.type = AppTreeNodeType.APP; + } + else + { + throw (new IllegalStateException("Unrecognized class for app child meta data: " + appChildMetaData.getClass())); + } + + if(appChildMetaData.getIcon() != null && StringUtils.hasContent(appChildMetaData.getIcon().getName())) + { + this.iconName = appChildMetaData.getIcon().getName(); + } + } + + + + /******************************************************************************* + ** Getter for name + ** + *******************************************************************************/ + public String getName() + { + return name; + } + + + + /******************************************************************************* + ** Getter for label + ** + *******************************************************************************/ + public String getLabel() + { + return label; + } + + + + /******************************************************************************* + ** Getter for type + ** + *******************************************************************************/ + public AppTreeNodeType getType() + { + return type; + } + + + + /******************************************************************************* + ** Getter for children + ** + *******************************************************************************/ + public List getChildren() + { + return children; + } + + + + /******************************************************************************* + ** Getter for iconName + ** + *******************************************************************************/ + public String getIconName() + { + return iconName; + } + + + + /******************************************************************************* + ** Setter for iconName + ** + *******************************************************************************/ + public void setIconName(String iconName) + { + this.iconName = iconName; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public void addChild(QFrontendAppMetaData qFrontendAppMetaData) + { + if(children == null) + { + children = new ArrayList<>(); + } + children.add(qFrontendAppMetaData); + } +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/layout/QAppChildMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/layout/QAppChildMetaData.java new file mode 100644 index 00000000..85db26ef --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/layout/QAppChildMetaData.java @@ -0,0 +1,58 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.model.metadata.layout; + + +/******************************************************************************* + ** Interface shared by meta-data objects which can be placed into an App. + *******************************************************************************/ +public interface QAppChildMetaData +{ + /******************************************************************************* + ** + *******************************************************************************/ + void setParentAppName(String parentAppName); + + /******************************************************************************* + ** + *******************************************************************************/ + String getParentAppName(); + + /******************************************************************************* + ** + *******************************************************************************/ + String getName(); + + /******************************************************************************* + ** + *******************************************************************************/ + String getLabel(); + + /******************************************************************************* + ** + *******************************************************************************/ + default QIcon getIcon() + { + return (null); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/layout/QAppMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/layout/QAppMetaData.java new file mode 100644 index 00000000..22681a47 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/layout/QAppMetaData.java @@ -0,0 +1,237 @@ +/* + * 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.layout; + + +import java.util.ArrayList; +import java.util.List; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class QAppMetaData implements QAppChildMetaData +{ + private String name; + private String label; + + private List children; + + private String parentAppName; + private QIcon icon; + + + + /******************************************************************************* + ** + *******************************************************************************/ + public QAppMetaData() + { + } + + + + /******************************************************************************* + ** Getter for name + ** + *******************************************************************************/ + public String getName() + { + return name; + } + + + + /******************************************************************************* + ** Setter for name + ** + *******************************************************************************/ + public void setName(String name) + { + this.name = name; + } + + + + /******************************************************************************* + ** Fluent setter for name + ** + *******************************************************************************/ + public QAppMetaData withName(String name) + { + this.name = name; + return (this); + } + + + + /******************************************************************************* + ** Getter for label + ** + *******************************************************************************/ + public String getLabel() + { + return label; + } + + + + /******************************************************************************* + ** Setter for label + ** + *******************************************************************************/ + public void setLabel(String label) + { + this.label = label; + } + + + + /******************************************************************************* + ** Fluent setter for label + ** + *******************************************************************************/ + public QAppMetaData withLabel(String label) + { + this.label = label; + return (this); + } + + + + /******************************************************************************* + ** Getter for children + ** + *******************************************************************************/ + public List getChildren() + { + return children; + } + + + + /******************************************************************************* + ** Setter for children + ** + *******************************************************************************/ + public void setChildren(List children) + { + this.children = children; + } + + + + /******************************************************************************* + ** Add a child to this app. + ** + *******************************************************************************/ + public void addChild(QAppChildMetaData child) + { + if(this.children == null) + { + this.children = new ArrayList<>(); + } + this.children.add(child); + child.setParentAppName(this.getName()); + } + + + + /******************************************************************************* + ** Fluently add a child to this app. + ** + *******************************************************************************/ + public QAppMetaData withChild(QAppChildMetaData child) + { + addChild(child); + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for children + ** + *******************************************************************************/ + public QAppMetaData withChildren(List children) + { + this.children = children; + return (this); + } + + + + /******************************************************************************* + ** Getter for parentAppName + ** + *******************************************************************************/ + @Override + public String getParentAppName() + { + return parentAppName; + } + + + + /******************************************************************************* + ** Setter for parentAppName + ** + *******************************************************************************/ + @Override + public void setParentAppName(String parentAppName) + { + this.parentAppName = parentAppName; + } + + + /******************************************************************************* + ** Getter for icon + ** + *******************************************************************************/ + public QIcon getIcon() + { + return icon; + } + + + + /******************************************************************************* + ** Setter for icon + ** + *******************************************************************************/ + public void setIcon(QIcon icon) + { + this.icon = icon; + } + + + /******************************************************************************* + ** Fluent setter for icon + ** + *******************************************************************************/ + public QAppMetaData withIcon(QIcon icon) + { + this.icon = icon; + return (this); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/layout/QIcon.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/layout/QIcon.java new file mode 100644 index 00000000..92e374c6 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/layout/QIcon.java @@ -0,0 +1,91 @@ +/* + * 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.layout; + + +/******************************************************************************* + ** Icon to show associated with an App, Table, Process, etc. + ** + ** Currently, name here must be a reference from https://fonts.google.com/icons + ** e.g., local_shipping for https://fonts.google.com/icons?selected=Material+Symbols+Outlined:local_shipping + ** + ** Future may allow something like a "namespace", and/or multiple icons for + ** use in different frontends, etc. + *******************************************************************************/ +public class QIcon +{ + private String name; + + + + /******************************************************************************* + ** + *******************************************************************************/ + public QIcon() + { + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public QIcon(String name) + { + this.name = name; + } + + + + /******************************************************************************* + ** Getter for name + ** + *******************************************************************************/ + public String getName() + { + return name; + } + + + + /******************************************************************************* + ** Setter for name + ** + *******************************************************************************/ + public void setName(String name) + { + this.name = name; + } + + + + /******************************************************************************* + ** Fluent setter for name + ** + *******************************************************************************/ + public QIcon withName(String name) + { + this.name = name; + return (this); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QProcessMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QProcessMetaData.java index 213eabd2..a63f66ba 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QProcessMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QProcessMetaData.java @@ -26,13 +26,15 @@ import java.util.ArrayList; import java.util.List; import com.fasterxml.jackson.annotation.JsonIgnore; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppChildMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData; /******************************************************************************* ** Meta-Data to define a process in a QQQ instance. ** *******************************************************************************/ -public class QProcessMetaData +public class QProcessMetaData implements QAppChildMetaData { private String name; private String label; @@ -41,6 +43,8 @@ public class QProcessMetaData private List stepList; + private String parentAppName; + /******************************************************************************* @@ -293,4 +297,28 @@ public class QProcessMetaData return (this); } + + + /******************************************************************************* + ** Getter for parentAppName + ** + *******************************************************************************/ + @Override + public String getParentAppName() + { + return parentAppName; + } + + + + /******************************************************************************* + ** Setter for parentAppName + ** + *******************************************************************************/ + @Override + public void setParentAppName(String parentAppName) + { + this.parentAppName = parentAppName; + } + } 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 49695979..48945e16 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 @@ -34,13 +34,15 @@ import com.kingsrook.qqq.backend.core.model.data.QRecordEntity; import com.kingsrook.qqq.backend.core.model.data.QRecordEntityField; 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.layout.QAppChildMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon; /******************************************************************************* ** Meta-Data to define a table in a QQQ instance. ** *******************************************************************************/ -public class QTableMetaData implements Serializable +public class QTableMetaData implements QAppChildMetaData, Serializable { private String name; private String label; @@ -63,6 +65,8 @@ public class QTableMetaData implements Serializable private Map customizers; + private String parentAppName; + private QIcon icon; /******************************************************************************* @@ -465,4 +469,61 @@ public class QTableMetaData implements Serializable return (this); } + + + /******************************************************************************* + ** Getter for parentAppName + ** + *******************************************************************************/ + @Override + public String getParentAppName() + { + return parentAppName; + } + + + + /******************************************************************************* + ** Setter for parentAppName + ** + *******************************************************************************/ + @Override + public void setParentAppName(String parentAppName) + { + this.parentAppName = parentAppName; + } + + + + /******************************************************************************* + ** Getter for icon + ** + *******************************************************************************/ + public QIcon getIcon() + { + return icon; + } + + + + /******************************************************************************* + ** Setter for icon + ** + *******************************************************************************/ + public void setIcon(QIcon icon) + { + this.icon = icon; + } + + + /******************************************************************************* + ** Fluent setter for icon + ** + *******************************************************************************/ + public QTableMetaData withIcon(QIcon icon) + { + this.icon = icon; + return (this); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/mock/MockQueryAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/mock/MockQueryAction.java index ff3761f7..ec5c3365 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/mock/MockQueryAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/mock/MockQueryAction.java @@ -65,6 +65,7 @@ public class MockQueryAction implements QueryInterface { Serializable value = field.equals("id") ? (i + 1) : getValue(table, field); record.setValue(field, value); + record.setDisplayValue(table.getField(field), value); } queryOutput.addRecord(record); diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/metadata/MetaDataActionTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/metadata/MetaDataActionTest.java index f66f81ea..c86ef97e 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/metadata/MetaDataActionTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/metadata/MetaDataActionTest.java @@ -22,13 +22,18 @@ package com.kingsrook.qqq.backend.core.actions.metadata; +import java.util.List; +import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataInput; import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataOutput; +import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendAppMetaData; import com.kingsrook.qqq.backend.core.utils.TestUtils; import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -51,11 +56,17 @@ class MetaDataActionTest MetaDataOutput result = new MetaDataAction().execute(request); assertNotNull(result); + /////////////////////////////////// + // assert against the tables map // + /////////////////////////////////// assertNotNull(result.getTables()); assertNotNull(result.getTables().get("person")); assertEquals("person", result.getTables().get("person").getName()); assertEquals("Person", result.getTables().get("person").getLabel()); + ////////////////////////////////////// + // assert against the processes map // + ////////////////////////////////////// assertNotNull(result.getProcesses().get("greet")); assertNotNull(result.getProcesses().get("greetInteractive")); assertNotNull(result.getProcesses().get("etl.basic")); @@ -63,5 +74,47 @@ class MetaDataActionTest assertNotNull(result.getProcesses().get("person.bulkEdit")); assertNotNull(result.getProcesses().get("person.bulkDelete")); + ///////////////////////////////////////////////////////////////////////////////////////////////////////// + // assert against the apps map - which is appName to app - but not fully hierarchical - that's appTree // + ///////////////////////////////////////////////////////////////////////////////////////////////////////// + Map apps = result.getApps(); + assertNotNull(apps.get(TestUtils.APP_NAME_GREETINGS)); + assertNotNull(apps.get(TestUtils.APP_NAME_PEOPLE)); + assertNotNull(apps.get(TestUtils.APP_NAME_MISCELLANEOUS)); + + QFrontendAppMetaData peopleApp = apps.get(TestUtils.APP_NAME_PEOPLE); + assertThat(peopleApp.getChildren()).isNotEmpty(); + Optional greetingsAppUnderPeopleFromMapOptional = peopleApp.getChildren().stream() + .filter(e -> e.getName().equals(TestUtils.APP_NAME_GREETINGS)).findFirst(); + assertThat(greetingsAppUnderPeopleFromMapOptional).isPresent(); + + ////////////////////////////////////////////////////////////////////////////// + // we want to show that in the appMap (e.g., "apps"), that the apps are not // + // hierarchical - that is - that a sub-app doesn't list ITS children here. // + ////////////////////////////////////////////////////////////////////////////// + assertThat(greetingsAppUnderPeopleFromMapOptional.get().getChildren()).isNullOrEmpty(); + + /////////////////////////////////////////////// + // assert against the hierarchical apps tree // + /////////////////////////////////////////////// + List appTree = result.getAppTree(); + Set appNamesInTopOfTree = appTree.stream().map(QFrontendAppMetaData::getName).collect(Collectors.toSet()); + assertThat(appNamesInTopOfTree).contains(TestUtils.APP_NAME_PEOPLE); + assertThat(appNamesInTopOfTree).contains(TestUtils.APP_NAME_MISCELLANEOUS); + assertThat(appNamesInTopOfTree).doesNotContain(TestUtils.APP_NAME_GREETINGS); + + Optional peopleAppOptional = appTree.stream() + .filter(e -> e.getName().equals(TestUtils.APP_NAME_PEOPLE)).findFirst(); + assertThat(peopleAppOptional).isPresent(); + assertThat(peopleAppOptional.get().getChildren()).isNotEmpty(); + + Optional greetingsAppUnderPeopleFromTree = peopleAppOptional.get().getChildren().stream() + .filter(e -> e.getName().equals(TestUtils.APP_NAME_GREETINGS)).findFirst(); + assertThat(greetingsAppUnderPeopleFromTree).isPresent(); + + ///////////////////////////////////////////////////////////////////////////////// + // but here, when this app comes from the tree, then it DOES have its children // + ///////////////////////////////////////////////////////////////////////////////// + assertThat(greetingsAppUnderPeopleFromTree.get().getChildren()).isNotEmpty(); } } 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 3917f7e7..326e0742 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 @@ -25,8 +25,10 @@ package com.kingsrook.qqq.backend.core.actions.tables; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput; +import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.utils.TestUtils; import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -50,5 +52,13 @@ class QueryActionTest request.setTableName("person"); QueryOutput result = new QueryAction().execute(request); assertNotNull(result); + + assertThat(result.getRecords()).isNotEmpty(); + for(QRecord record : result.getRecords()) + { + assertThat(record.getValues()).isNotEmpty(); + assertThat(record.getDisplayValues()).isNotEmpty(); + assertThat(record.getErrors()).isEmpty(); + } } } 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 c2bc70e9..743f43ef 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,13 +22,17 @@ package com.kingsrook.qqq.backend.core.instances; +import java.util.Collections; import java.util.HashMap; +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.layout.QAppMetaData; import com.kingsrook.qqq.backend.core.utils.TestUtils; import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; @@ -51,6 +55,21 @@ class QInstanceValidatorTest + /******************************************************************************* + ** make sure we don't re-validate if already validated + ** + *******************************************************************************/ + @Test + public void test_doNotReValidate() throws QInstanceValidationException + { + QInstance qInstance = TestUtils.defineInstance(); + qInstance.setHasBeenValidated(new QInstanceValidationKey()); + qInstance.setBackends(null); + new QInstanceValidator().validate(qInstance); + } + + + /******************************************************************************* ** Test an instance with null backends - should throw. ** @@ -58,17 +77,8 @@ class QInstanceValidatorTest @Test public void test_validateNullBackends() { - try - { - QInstance qInstance = TestUtils.defineInstance(); - qInstance.setBackends(null); - new QInstanceValidator().validate(qInstance); - fail("Should have thrown validationException"); - } - catch(QInstanceValidationException e) - { - assertReason("At least 1 backend must be defined", e); - } + assertValidationFailureReasons((qInstance) -> qInstance.setBackends(null), + "At least 1 backend must be defined"); } @@ -80,17 +90,8 @@ class QInstanceValidatorTest @Test public void test_validateEmptyBackends() { - try - { - QInstance qInstance = TestUtils.defineInstance(); - qInstance.setBackends(new HashMap<>()); - new QInstanceValidator().validate(qInstance); - fail("Should have thrown validationException"); - } - catch(QInstanceValidationException e) - { - assertReason("At least 1 backend must be defined", e); - } + assertValidationFailureReasons((qInstance) -> qInstance.setBackends(new HashMap<>()), + "At least 1 backend must be defined"); } @@ -102,17 +103,12 @@ class QInstanceValidatorTest @Test public void test_validateNullTables() { - try - { - QInstance qInstance = TestUtils.defineInstance(); - qInstance.setTables(null); - new QInstanceValidator().validate(qInstance); - fail("Should have thrown validationException"); - } - catch(QInstanceValidationException e) - { - assertReason("At least 1 table must be defined", e); - } + assertValidationFailureReasons((qInstance) -> + { + qInstance.setTables(null); + qInstance.setProcesses(null); + }, + "At least 1 table must be defined"); } @@ -124,17 +120,12 @@ class QInstanceValidatorTest @Test public void test_validateEmptyTables() { - try - { - QInstance qInstance = TestUtils.defineInstance(); - qInstance.setTables(new HashMap<>()); - new QInstanceValidator().validate(qInstance); - fail("Should have thrown validationException"); - } - catch(QInstanceValidationException e) - { - assertReason("At least 1 table must be defined", e); - } + assertValidationFailureReasons((qInstance) -> + { + qInstance.setTables(new HashMap<>()); + qInstance.setProcesses(new HashMap<>()); + }, + "At least 1 table must be defined"); } @@ -147,19 +138,15 @@ class QInstanceValidatorTest @Test public void test_validateInconsistentNames() { - try - { - QInstance qInstance = TestUtils.defineInstance(); - qInstance.getTable("person").setName("notPerson"); - qInstance.getBackend("default").setName("notDefault"); - new QInstanceValidator().validate(qInstance); - fail("Should have thrown validationException"); - } - catch(QInstanceValidationException e) - { - assertReason("Inconsistent naming for table", e); - assertReason("Inconsistent naming for backend", e); - } + assertValidationFailureReasonsAllowingExtraReasons((qInstance) -> + { + qInstance.getTable("person").setName("notPerson"); + qInstance.getBackend("default").setName("notDefault"); + qInstance.getProcess(TestUtils.PROCESS_NAME_GREET_PEOPLE).setName("notGreetPeople"); + }, + "Inconsistent naming for table", + "Inconsistent naming for backend", + "Inconsistent naming for process"); } @@ -171,17 +158,8 @@ class QInstanceValidatorTest @Test public void test_validateTableWithoutBackend() { - try - { - QInstance qInstance = TestUtils.defineInstance(); - qInstance.getTable("person").setBackendName(null); - new QInstanceValidator().validate(qInstance); - fail("Should have thrown validationException"); - } - catch(QInstanceValidationException e) - { - assertReason("Missing backend name for table", e); - } + assertValidationFailureReasons((qInstance) -> qInstance.getTable("person").setBackendName(null), + "Missing backend name for table"); } @@ -193,17 +171,53 @@ class QInstanceValidatorTest @Test public void test_validateTableWithMissingBackend() { - try - { - QInstance qInstance = TestUtils.defineInstance(); - qInstance.getTable("person").setBackendName("notARealBackend"); - new QInstanceValidator().validate(qInstance); - fail("Should have thrown validationException"); - } - catch(QInstanceValidationException e) - { - assertReason("Unrecognized backend", e); - } + assertValidationFailureReasons((qInstance) -> qInstance.getTable("person").setBackendName("notARealBackend"), + "Unrecognized backend"); + } + + + + /******************************************************************************* + ** Test that if a process specifies a table that doesn't exist, that it fails. + ** + *******************************************************************************/ + @Test + public void test_validateProcessWithMissingTable() + { + assertValidationFailureReasons((qInstance) -> qInstance.getProcess(TestUtils.PROCESS_NAME_GREET_PEOPLE).setTableName("notATableName"), + "Unrecognized table"); + } + + + + /******************************************************************************* + ** Test that a process with no steps fails + ** + *******************************************************************************/ + @Test + public void test_validateProcessWithNoSteps() + { + assertValidationFailureReasons((qInstance) -> qInstance.getProcess(TestUtils.PROCESS_NAME_GREET_PEOPLE).setStepList(Collections.emptyList()), + "At least 1 step"); + + assertValidationFailureReasons((qInstance) -> qInstance.getProcess(TestUtils.PROCESS_NAME_GREET_PEOPLE).setStepList(null), + "At least 1 step"); + } + + + + /******************************************************************************* + ** Test that a process step with an empty string name fails + ** + *******************************************************************************/ + @Test + public void test_validateProcessStepWithEmptyName() + { + assertValidationFailureReasons((qInstance) -> qInstance.getProcess(TestUtils.PROCESS_NAME_GREET_PEOPLE).getStepList().get(0).setName(""), + "Missing name for a step"); + + assertValidationFailureReasons((qInstance) -> qInstance.getProcess(TestUtils.PROCESS_NAME_GREET_PEOPLE_INTERACTIVE).getStepList().get(1).setName(null), + "Missing name for a step"); } @@ -215,29 +229,11 @@ class QInstanceValidatorTest @Test public void test_validateTableWithNoFields() { - try - { - QInstance qInstance = TestUtils.defineInstance(); - qInstance.getTable("person").setFields(null); - new QInstanceValidator().validate(qInstance); - fail("Should have thrown validationException"); - } - catch(QInstanceValidationException e) - { - assertReason("At least 1 field", e); - } + assertValidationFailureReasons((qInstance) -> qInstance.getTable("person").setFields(null), + "At least 1 field"); - try - { - QInstance qInstance = TestUtils.defineInstance(); - qInstance.getTable("person").setFields(new HashMap<>()); - new QInstanceValidator().validate(qInstance); - fail("Should have thrown validationException"); - } - catch(QInstanceValidationException e) - { - assertReason("At least 1 field", e); - } + assertValidationFailureReasons((qInstance) -> qInstance.getTable("person").setFields(new HashMap<>()), + "At least 1 field"); } @@ -248,17 +244,92 @@ class QInstanceValidatorTest *******************************************************************************/ @Test public void test_validateFieldWithMissingPossibleValueSource() + { + assertValidationFailureReasons((qInstance) -> qInstance.getTable("person").getField("homeState").setPossibleValueSourceName("not a real possible value source"), + "Unrecognized possibleValueSourceName"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testChildrenWithBadParentAppName() + { + String[] reasons = new String[] { "Unrecognized parent app", "does not have its parent app properly set" }; + assertValidationFailureReasons((qInstance) -> qInstance.getTable(TestUtils.TABLE_NAME_PERSON).setParentAppName("notAnApp"), reasons); + assertValidationFailureReasons((qInstance) -> qInstance.getProcess(TestUtils.PROCESS_NAME_GREET_PEOPLE).setParentAppName("notAnApp"), reasons); + assertValidationFailureReasons((qInstance) -> qInstance.getApp(TestUtils.APP_NAME_GREETINGS).setParentAppName("notAnApp"), reasons); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testAppCircularReferences() + { + assertValidationFailureReasonsAllowingExtraReasons((qInstance) -> + { + QAppMetaData miscApp = qInstance.getApp(TestUtils.APP_NAME_MISCELLANEOUS); + QAppMetaData greetingsApp = qInstance.getApp(TestUtils.APP_NAME_GREETINGS); + + miscApp.withChild(greetingsApp); + greetingsApp.withChild(miscApp); + }, "Circular app reference"); + } + + + + /******************************************************************************* + ** 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 + ** more reasons - e.g., helpful when one thing we're testing causes other errors). + *******************************************************************************/ + private void assertValidationFailureReasonsAllowingExtraReasons(Consumer setup, String... reasons) + { + assertValidationFailureReasons(setup, true, reasons); + } + + + + /******************************************************************************* + ** 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 (and + ** require that exact # of reasons). + *******************************************************************************/ + private void assertValidationFailureReasons(Consumer setup, String... reasons) + { + assertValidationFailureReasons(setup, false, reasons); + } + + + + /******************************************************************************* + ** Implementation for the overloads of this name. + *******************************************************************************/ + private void assertValidationFailureReasons(Consumer setup, boolean allowExtraReasons, String... reasons) { try { QInstance qInstance = TestUtils.defineInstance(); - qInstance.getTable("person").getField("homeState").setPossibleValueSourceName("not a real possible value source"); + setup.accept(qInstance); new QInstanceValidator().validate(qInstance); fail("Should have thrown validationException"); } catch(QInstanceValidationException e) { - assertReason("Unrecognized possibleValueSourceName", e); + if(!allowExtraReasons) + { + assertEquals(reasons.length, e.getReasons().size(), "Expected number of validation failure reasons\nExpected: " + String.join(",", reasons) + "\nActual: " + e.getReasons()); + } + + for(String reason : reasons) + { + assertReason(reason, e); + } } } @@ -271,7 +342,9 @@ class QInstanceValidatorTest *******************************************************************************/ private void assertReason(String reason, QInstanceValidationException e) { - assertNotNull(e.getReasons()); - assertTrue(e.getReasons().stream().anyMatch(s -> s.contains(reason))); + assertNotNull(e.getReasons(), "Expected there to be a reason for the failure (but there was not)"); + assertThat(e.getReasons()) + .withFailMessage("Expected any of:\n%s\nTo match: [%s]", e.getReasons(), reason) + .anyMatch(s -> s.contains(reason)); } } 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 3fc3e6b0..a0711f87 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 @@ -39,6 +39,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeUsage; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; +import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData; import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource; import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType; import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData; @@ -53,8 +54,8 @@ import com.kingsrook.qqq.backend.core.modules.authentication.MockAuthenticationM import com.kingsrook.qqq.backend.core.modules.authentication.metadata.QAuthenticationMetaData; import com.kingsrook.qqq.backend.core.modules.backend.implementations.mock.MockBackendModule; import com.kingsrook.qqq.backend.core.processes.implementations.etl.basic.BasicETLProcess; -import com.kingsrook.qqq.backend.core.processes.implementations.mock.MockBackendStep; import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamed.StreamedETLProcess; +import com.kingsrook.qqq.backend.core.processes.implementations.mock.MockBackendStep; /******************************************************************************* @@ -65,6 +66,10 @@ public class TestUtils { public static final String DEFAULT_BACKEND_NAME = "default"; + public static final String APP_NAME_GREETINGS = "greetingsApp"; + public static final String APP_NAME_PEOPLE = "peopleApp"; + public static final String APP_NAME_MISCELLANEOUS = "miscellaneous"; + public static final String TABLE_NAME_PERSON = "person"; public static final String PROCESS_NAME_GREET_PEOPLE = "greet"; @@ -84,16 +89,21 @@ public class TestUtils QInstance qInstance = new QInstance(); qInstance.setAuthentication(defineAuthentication()); qInstance.addBackend(defineBackend()); + qInstance.addTable(defineTablePerson()); qInstance.addTable(definePersonFileTable()); qInstance.addTable(defineTableIdAndNameOnly()); + qInstance.addPossibleValueSource(defineStatesPossibleValueSource()); + qInstance.addProcess(defineProcessGreetPeople()); qInstance.addProcess(defineProcessGreetPeopleInteractive()); qInstance.addProcess(defineProcessAddToPeoplesAge()); qInstance.addProcess(new BasicETLProcess().defineProcessMetaData()); qInstance.addProcess(new StreamedETLProcess().defineProcessMetaData()); + defineApps(qInstance); + System.out.println(new QInstanceAdapter().qInstanceToJson(qInstance)); return (qInstance); @@ -101,6 +111,30 @@ public class TestUtils + /******************************************************************************* + ** + *******************************************************************************/ + private static void defineApps(QInstance qInstance) + { + qInstance.addApp(new QAppMetaData() + .withName(APP_NAME_GREETINGS) + .withChild(qInstance.getProcess(PROCESS_NAME_GREET_PEOPLE)) + .withChild(qInstance.getProcess(PROCESS_NAME_GREET_PEOPLE_INTERACTIVE))); + + qInstance.addApp(new QAppMetaData() + .withName(APP_NAME_PEOPLE) + .withChild(qInstance.getTable(TABLE_NAME_PERSON)) + .withChild(qInstance.getTable(TABLE_NAME_PERSON_FILE)) + .withChild(qInstance.getApp(APP_NAME_GREETINGS))); + + qInstance.addApp(new QAppMetaData() + .withName(APP_NAME_MISCELLANEOUS) + .withChild(qInstance.getTable(TABLE_NAME_ID_AND_NAME_ONLY)) + .withChild(qInstance.getProcess(BasicETLProcess.PROCESS_NAME))); + } + + + /******************************************************************************* ** Define the "states" possible value source used in standard tests ** 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 b67241ee..3e1fcf77 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 @@ -46,10 +46,12 @@ import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction; import com.kingsrook.qqq.backend.core.adapters.QInstanceAdapter; import com.kingsrook.qqq.backend.core.exceptions.QAuthenticationException; +import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException; import com.kingsrook.qqq.backend.core.exceptions.QModuleDispatchException; import com.kingsrook.qqq.backend.core.exceptions.QNotFoundException; import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException; import com.kingsrook.qqq.backend.core.exceptions.QValueException; +import com.kingsrook.qqq.backend.core.instances.QInstanceValidator; import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput; import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataInput; import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataOutput; @@ -121,7 +123,7 @@ public class QJavalinImplementation /******************************************************************************* ** *******************************************************************************/ - public static void main(String[] args) + public static void main(String[] args) throws QInstanceValidationException { QInstance qInstance = new QInstance(); // todo - parse args to look up metaData and prime instance @@ -135,9 +137,10 @@ public class QJavalinImplementation /******************************************************************************* ** *******************************************************************************/ - public QJavalinImplementation(QInstance qInstance) + public QJavalinImplementation(QInstance qInstance) throws QInstanceValidationException { QJavalinImplementation.qInstance = qInstance; + new QInstanceValidator().validate(qInstance); } diff --git a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinTestBase.java b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinTestBase.java index 168174c2..571013fa 100644 --- a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinTestBase.java +++ b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinTestBase.java @@ -22,6 +22,7 @@ package com.kingsrook.qqq.backend.javalin; +import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; @@ -44,7 +45,7 @@ public class QJavalinTestBase ** *******************************************************************************/ @BeforeAll - public static void beforeAll() + public static void beforeAll() throws QInstanceValidationException { qJavalinImplementation = new QJavalinImplementation(TestUtils.defineInstance()); QJavalinProcessHandler.setAsyncStepTimeoutMillis(250); diff --git a/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleJavalinServer.java b/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleJavalinServer.java index 434b4362..aa5b049b 100644 --- a/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleJavalinServer.java +++ b/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleJavalinServer.java @@ -22,6 +22,7 @@ package com.kingsrook.sampleapp; +import com.kingsrook.qqq.backend.core.instances.QInstanceValidator; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.javalin.QJavalinImplementation; import io.javalin.Javalin; 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 a86b7ad4..87458a05 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 @@ -29,13 +29,13 @@ import com.kingsrook.qqq.backend.core.exceptions.QValueException; import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput; import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput; import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType; -import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeUsage; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; +import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionInputMetaData; @@ -59,7 +59,7 @@ import io.github.cdimascio.dotenv.Dotenv; *******************************************************************************/ public class SampleMetaDataProvider { - public static boolean USE_MYSQL = false; + public static boolean USE_MYSQL = true; public static final String RDBMS_BACKEND_NAME = "rdbms"; public static final String FILESYSTEM_BACKEND_NAME = "filesystem"; @@ -68,12 +68,20 @@ public class SampleMetaDataProvider // public static final String AUTH0_BASE_URL = "https://kingsrook.us.auth0.com/"; public static final String AUTH0_BASE_URL = "https://nutrifresh-one-development.us.auth0.com/"; + public static final String APP_NAME_GREETINGS = "greetingsApp"; + public static final String APP_NAME_PEOPLE = "peopleApp"; + public static final String APP_NAME_MISCELLANEOUS = "miscellaneous"; + public static final String PROCESS_NAME_GREET = "greet"; public static final String PROCESS_NAME_GREET_INTERACTIVE = "greetInteractive"; public static final String PROCESS_NAME_SIMPLE_SLEEP = "simpleSleep"; public static final String PROCESS_NAME_SIMPLE_THROW = "simpleThrow"; public static final String PROCESS_NAME_SLEEP_INTERACTIVE = "sleepInteractive"; + public static final String TABLE_NAME_PERSON = "person"; + public static final String TABLE_NAME_CARRIER = "carrier"; + public static final String TABLE_NAME_CITY = "city"; + public static final String STEP_NAME_SLEEPER = "sleeper"; public static final String STEP_NAME_THROWER = "thrower"; @@ -101,11 +109,39 @@ public class SampleMetaDataProvider qInstance.addProcess(defineProcessScreenThenSleep()); qInstance.addProcess(defineProcessSimpleThrow()); + defineApps(qInstance); + return (qInstance); } + /******************************************************************************* + ** + *******************************************************************************/ + private static void defineApps(QInstance qInstance) + { + qInstance.addApp(new QAppMetaData() + .withName(APP_NAME_GREETINGS) + .withChild(qInstance.getProcess(PROCESS_NAME_GREET)) + .withChild(qInstance.getProcess(PROCESS_NAME_GREET_INTERACTIVE))); + + qInstance.addApp(new QAppMetaData() + .withName(APP_NAME_PEOPLE) + .withChild(qInstance.getTable(TABLE_NAME_PERSON)) + .withChild(qInstance.getTable(TABLE_NAME_CITY)) + .withChild(qInstance.getApp(APP_NAME_GREETINGS))); + + qInstance.addApp(new QAppMetaData() + .withName(APP_NAME_MISCELLANEOUS) + .withChild(qInstance.getTable(TABLE_NAME_CARRIER)) + .withChild(qInstance.getProcess(PROCESS_NAME_SIMPLE_SLEEP)) + .withChild(qInstance.getProcess(PROCESS_NAME_SLEEP_INTERACTIVE)) + .withChild(qInstance.getProcess(PROCESS_NAME_SIMPLE_THROW))); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -166,7 +202,7 @@ public class SampleMetaDataProvider public static QTableMetaData defineTableCarrier() { QTableMetaData table = new QTableMetaData(); - table.setName("carrier"); + table.setName(TABLE_NAME_CARRIER); table.setBackendName(RDBMS_BACKEND_NAME); table.setPrimaryKeyField("id"); @@ -194,7 +230,7 @@ public class SampleMetaDataProvider public static QTableMetaData defineTablePerson() { return new QTableMetaData() - .withName("person") + .withName(TABLE_NAME_PERSON) .withLabel("Person") .withBackendName(RDBMS_BACKEND_NAME) .withPrimaryKeyField("id") @@ -215,7 +251,7 @@ public class SampleMetaDataProvider public static QTableMetaData defineTableCityFile() { return new QTableMetaData() - .withName("city") + .withName(TABLE_NAME_CITY) .withLabel("Cities") .withIsHidden(true) .withBackendName(FILESYSTEM_BACKEND_NAME) @@ -240,7 +276,7 @@ public class SampleMetaDataProvider return new QProcessMetaData() .withName(PROCESS_NAME_GREET) .withLabel("Greet People") - .withTableName("person") + .withTableName(TABLE_NAME_PERSON) .withIsHidden(true) .addStep(new QBackendStepMetaData() .withName("prepare") @@ -249,14 +285,14 @@ public class SampleMetaDataProvider .withCodeType(QCodeType.JAVA) .withCodeUsage(QCodeUsage.BACKEND_STEP)) // todo - needed, or implied in this context? .withInputData(new QFunctionInputMetaData() - .withRecordListMetaData(new QRecordListMetaData().withTableName("person")) + .withRecordListMetaData(new QRecordListMetaData().withTableName(TABLE_NAME_PERSON)) .withFieldList(List.of( new QFieldMetaData("greetingPrefix", QFieldType.STRING), new QFieldMetaData("greetingSuffix", QFieldType.STRING) ))) .withOutputMetaData(new QFunctionOutputMetaData() .withRecordListMetaData(new QRecordListMetaData() - .withTableName("person") + .withTableName(TABLE_NAME_PERSON) .addField(new QFieldMetaData("fullGreeting", QFieldType.STRING)) ) .withFieldList(List.of(new QFieldMetaData("outputMessage", QFieldType.STRING)))) @@ -272,9 +308,9 @@ public class SampleMetaDataProvider { return new QProcessMetaData() .withName(PROCESS_NAME_GREET_INTERACTIVE) - .withTableName("person") + .withTableName(TABLE_NAME_PERSON) - .addStep(LoadInitialRecordsStep.defineMetaData("person")) + .addStep(LoadInitialRecordsStep.defineMetaData(TABLE_NAME_PERSON)) .addStep(new QFrontendStepMetaData() .withName("setup") @@ -289,14 +325,14 @@ public class SampleMetaDataProvider .withCodeType(QCodeType.JAVA) .withCodeUsage(QCodeUsage.BACKEND_STEP)) // todo - needed, or implied in this context? .withInputData(new QFunctionInputMetaData() - .withRecordListMetaData(new QRecordListMetaData().withTableName("person")) + .withRecordListMetaData(new QRecordListMetaData().withTableName(TABLE_NAME_PERSON)) .withFieldList(List.of( new QFieldMetaData("greetingPrefix", QFieldType.STRING), new QFieldMetaData("greetingSuffix", QFieldType.STRING) ))) .withOutputMetaData(new QFunctionOutputMetaData() .withRecordListMetaData(new QRecordListMetaData() - .withTableName("person") + .withTableName(TABLE_NAME_PERSON) .addField(new QFieldMetaData("fullGreeting", QFieldType.STRING)) ) .withFieldList(List.of(new QFieldMetaData("outputMessage", QFieldType.STRING))))