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 cf6afa63..79fa519e 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 @@ -57,6 +57,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendComponen import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QSupplementalProcessMetaData; import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportDataSource; import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData; import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportView; @@ -367,6 +368,11 @@ public class QInstanceEnricher process.getStepList().forEach(this::enrichStep); } + for(QSupplementalProcessMetaData supplementalProcessMetaData : CollectionUtils.nonNullMap(process.getSupplementalMetaData()).values()) + { + supplementalProcessMetaData.enrich(process); + } + enrichPermissionRules(process); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunProcessInput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunProcessInput.java index 15e0e063..ec7b7cc0 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunProcessInput.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunProcessInput.java @@ -23,6 +23,10 @@ package com.kingsrook.qqq.backend.core.model.actions.processes; import java.io.Serializable; +import java.math.BigDecimal; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalTime; import java.util.List; import java.util.Map; import com.kingsrook.qqq.backend.core.actions.async.AsyncJobCallback; @@ -31,6 +35,7 @@ import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput; import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; +import com.kingsrook.qqq.backend.core.utils.ValueUtils; /******************************************************************************* @@ -183,6 +188,17 @@ public class RunProcessInput extends AbstractActionInput + /******************************************************************************* + ** + *******************************************************************************/ + public RunProcessInput withValue(String fieldName, Serializable value) + { + this.processState.getValues().put(fieldName, value); + return (this); + } + + + /******************************************************************************* ** Setter for values ** @@ -258,7 +274,7 @@ public class RunProcessInput extends AbstractActionInput *******************************************************************************/ public String getValueString(String fieldName) { - return ((String) getValue(fieldName)); + return (ValueUtils.getValueAsString(getValue(fieldName))); } @@ -269,7 +285,67 @@ public class RunProcessInput extends AbstractActionInput *******************************************************************************/ public Integer getValueInteger(String fieldName) { - return ((Integer) getValue(fieldName)); + return (ValueUtils.getValueAsInteger(getValue(fieldName))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public BigDecimal getValueBigDecimal(String fieldName) + { + return (ValueUtils.getValueAsBigDecimal(getValue(fieldName))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public Boolean getValueBoolean(String fieldName) + { + return (ValueUtils.getValueAsBoolean(getValue(fieldName))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public LocalTime getValueLocalTime(String fieldName) + { + return (ValueUtils.getValueAsLocalTime(getValue(fieldName))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public LocalDate getValueLocalDate(String fieldName) + { + return (ValueUtils.getValueAsLocalDate(getValue(fieldName))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public byte[] getValueByteArray(String fieldName) + { + return (ValueUtils.getValueAsByteArray(getValue(fieldName))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public Instant getValueInstant(String fieldName) + { + return (ValueUtils.getValueAsInstant(getValue(fieldName))); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunProcessOutput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunProcessOutput.java index c088f48b..466e02c4 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunProcessOutput.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunProcessOutput.java @@ -23,11 +23,16 @@ package com.kingsrook.qqq.backend.core.model.actions.processes; import java.io.Serializable; +import java.math.BigDecimal; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalTime; import java.util.List; import java.util.Map; import java.util.Optional; import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput; import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.utils.ValueUtils; /******************************************************************************* @@ -122,6 +127,99 @@ public class RunProcessOutput extends AbstractActionOutput implements Serializab + /******************************************************************************* + ** Getter for a single field's value + ** + *******************************************************************************/ + public Serializable getValue(String fieldName) + { + return (this.processState.getValues().get(fieldName)); + } + + + + /******************************************************************************* + ** Getter for a single field's value + ** + *******************************************************************************/ + public String getValueString(String fieldName) + { + return (ValueUtils.getValueAsString(getValue(fieldName))); + } + + + + /******************************************************************************* + ** Getter for a single field's value + ** + *******************************************************************************/ + public Integer getValueInteger(String fieldName) + { + return (ValueUtils.getValueAsInteger(getValue(fieldName))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public BigDecimal getValueBigDecimal(String fieldName) + { + return (ValueUtils.getValueAsBigDecimal(getValue(fieldName))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public Boolean getValueBoolean(String fieldName) + { + return (ValueUtils.getValueAsBoolean(getValue(fieldName))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public LocalTime getValueLocalTime(String fieldName) + { + return (ValueUtils.getValueAsLocalTime(getValue(fieldName))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public LocalDate getValueLocalDate(String fieldName) + { + return (ValueUtils.getValueAsLocalDate(getValue(fieldName))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public byte[] getValueByteArray(String fieldName) + { + return (ValueUtils.getValueAsByteArray(getValue(fieldName))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public Instant getValueInstant(String fieldName) + { + return (ValueUtils.getValueAsInstant(getValue(fieldName))); + } + + + /******************************************************************************* ** Setter for values ** @@ -133,6 +231,17 @@ public class RunProcessOutput extends AbstractActionOutput implements Serializab + /******************************************************************************* + ** + *******************************************************************************/ + public RunProcessOutput withValue(String fieldName, Serializable value) + { + this.processState.getValues().put(fieldName, value); + return (this); + } + + + /******************************************************************************* ** Setter for values ** 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 4f29ac04..5295cad9 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 @@ -61,6 +61,7 @@ public class QProcessMetaData implements QAppChildMetaData, MetaDataWithPermissi private QScheduleMetaData schedule; + private Map supplementalMetaData; /******************************************************************************* @@ -544,4 +545,64 @@ public class QProcessMetaData implements QAppChildMetaData, MetaDataWithPermissi qInstance.addProcess(this); } + + + /******************************************************************************* + ** Getter for supplementalMetaData + *******************************************************************************/ + public Map getSupplementalMetaData() + { + return (this.supplementalMetaData); + } + + + + /******************************************************************************* + ** Getter for supplementalMetaData + *******************************************************************************/ + public QSupplementalProcessMetaData getSupplementalMetaData(String type) + { + if(this.supplementalMetaData == null) + { + return (null); + } + return this.supplementalMetaData.get(type); + } + + + + /******************************************************************************* + ** Setter for supplementalMetaData + *******************************************************************************/ + public void setSupplementalMetaData(Map supplementalMetaData) + { + this.supplementalMetaData = supplementalMetaData; + } + + + + /******************************************************************************* + ** Fluent setter for supplementalMetaData + *******************************************************************************/ + public QProcessMetaData withSupplementalMetaData(Map supplementalMetaData) + { + this.supplementalMetaData = supplementalMetaData; + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for supplementalMetaData + *******************************************************************************/ + public QProcessMetaData withSupplementalMetaData(QSupplementalProcessMetaData supplementalMetaData) + { + if(this.supplementalMetaData == null) + { + this.supplementalMetaData = new HashMap<>(); + } + this.supplementalMetaData.put(supplementalMetaData.getType(), supplementalMetaData); + return (this); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QSupplementalProcessMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QSupplementalProcessMetaData.java new file mode 100644 index 00000000..c60e01b3 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QSupplementalProcessMetaData.java @@ -0,0 +1,78 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. 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.processes; + + +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; + + +/******************************************************************************* + ** Base-class for process-level meta-data defined by some supplemental module, etc, + ** outside of qqq core + *******************************************************************************/ +public abstract class QSupplementalProcessMetaData +{ + protected String type; + + + + /******************************************************************************* + ** Getter for type + *******************************************************************************/ + public String getType() + { + return (this.type); + } + + + + /******************************************************************************* + ** Setter for type + *******************************************************************************/ + public void setType(String type) + { + this.type = type; + } + + + + /******************************************************************************* + ** Fluent setter for type + *******************************************************************************/ + public QSupplementalProcessMetaData withType(String type) + { + this.type = type; + return (this); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public void enrich(QProcessMetaData process) + { + //////////////////////// + // noop in base class // + //////////////////////// + } +} diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/ApiImplementation.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/ApiImplementation.java index 96660f60..7c94f10f 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/ApiImplementation.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/ApiImplementation.java @@ -30,14 +30,23 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.UUID; import com.kingsrook.qqq.api.javalin.QBadRequestException; import com.kingsrook.qqq.api.model.APIVersion; +import com.kingsrook.qqq.api.model.APIVersionRange; import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaData; import com.kingsrook.qqq.api.model.metadata.ApiOperation; +import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessCustomizers; +import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessMetaData; +import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessMetaDataContainer; +import com.kingsrook.qqq.api.model.metadata.processes.PostRunApiProcessCustomizer; +import com.kingsrook.qqq.api.model.metadata.processes.PreRunApiProcessCustomizer; import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaData; import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaDataContainer; +import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader; import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper; import com.kingsrook.qqq.backend.core.actions.permissions.TablePermissionSubType; +import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction; import com.kingsrook.qqq.backend.core.actions.tables.CountAction; import com.kingsrook.qqq.backend.core.actions.tables.DeleteAction; import com.kingsrook.qqq.backend.core.actions.tables.GetAction; @@ -49,6 +58,8 @@ import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QNotFoundException; import com.kingsrook.qqq.backend.core.logging.LogPair; import com.kingsrook.qqq.backend.core.logging.QLogger; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput; import com.kingsrook.qqq.backend.core.model.actions.tables.QInputSource; import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput; import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput; @@ -67,8 +78,10 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput; import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput; import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateOutput; import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.model.metadata.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.processes.QProcessMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.statusmessages.BadInputStatusMessage; import com.kingsrook.qqq.backend.core.model.statusmessages.NotFoundStatusMessage; @@ -96,10 +109,11 @@ public class ApiImplementation { private static final QLogger LOG = QLogger.getLogger(ApiImplementation.class); - ///////////////////////////////////// - // key: Pair // - ///////////////////////////////////// - private static Map, Map> tableApiNameMap = new HashMap<>(); + /////////////////////////////////////////////////////////////////// + // key: Pair, value: Map metaData> // + /////////////////////////////////////////////////////////////////// + private static Map, Map> tableApiNameMap = new HashMap<>(); + private static Map, Map> processApiNameMap = new HashMap<>(); @@ -896,6 +910,99 @@ public class ApiImplementation + /******************************************************************************* + ** + *******************************************************************************/ + public static Map runProcess(ApiInstanceMetaData apiInstanceMetaData, String version, String processApiName, Map paramMap) throws QException + { + QProcessMetaData process = validateProcessAndVersion(apiInstanceMetaData, version, processApiName); + String processName = process.getName(); + ApiProcessMetaData apiProcessMetaData = getApiProcessMetaDataIfProcessIsInApi(apiInstanceMetaData, process); + + List badRequestMessages = new ArrayList<>(); + Map output = new LinkedHashMap<>(); + + String processUUID = UUID.randomUUID().toString(); + + RunProcessInput runProcessInput = new RunProcessInput(); + runProcessInput.setProcessName(processName); + runProcessInput.setFrontendStepBehavior(RunProcessInput.FrontendStepBehavior.SKIP); + runProcessInput.setProcessUUID(processUUID); + // todo i don't think runProcessInput.setCallback(); + // todo i don't think runProcessInput.setAsyncJobCallback(); + + ////////////////////// + // map input values // + ////////////////////// + for(QFieldMetaData inputField : CollectionUtils.nonNullList(apiProcessMetaData.getInputFields())) + { + String value = paramMap.get(inputField.getName()); + if(!StringUtils.hasContent(value) && inputField.getIsRequired()) + { + badRequestMessages.add("Missing value for required input field " + inputField.getName()); + continue; + } + + // todo - types? + + runProcessInput.addValue(inputField.getName(), value); + } + + // todo! runProcessInput.setRecords(records); + + ///////////////////////////////////////// + // throw if bad inputs have been noted // + ///////////////////////////////////////// + if(!badRequestMessages.isEmpty()) + { + if(badRequestMessages.size() == 1) + { + throw (new QBadRequestException(badRequestMessages.get(0))); + } + else + { + throw (new QBadRequestException("Request failed with " + badRequestMessages.size() + " reasons: " + StringUtils.join(" \n", badRequestMessages))); + } + } + + ///////////////////////////////////////// + // run pre-customizer, if there is one // + ///////////////////////////////////////// + Map customizers = apiProcessMetaData.getCustomizers(); + if(customizers != null && customizers.containsKey(ApiProcessCustomizers.PRE_RUN.getRole())) + { + PreRunApiProcessCustomizer preRunCustomizer = QCodeLoader.getAdHoc(PreRunApiProcessCustomizer.class, customizers.get(ApiProcessCustomizers.PRE_RUN.getRole())); + preRunCustomizer.preApiRun(runProcessInput); + } + + ///////////////////// + // run the process // + ///////////////////// + RunProcessAction runProcessAction = new RunProcessAction(); + RunProcessOutput runProcessOutput = runProcessAction.execute(runProcessInput); + + ///////////////////////////////////////// + // run post-customizer, if there is one // + ///////////////////////////////////////// + if(customizers != null && customizers.containsKey(ApiProcessCustomizers.POST_RUN.getRole())) + { + PostRunApiProcessCustomizer postRunCustomizer = QCodeLoader.getAdHoc(PostRunApiProcessCustomizer.class, customizers.get(ApiProcessCustomizers.POST_RUN.getRole())); + postRunCustomizer.postApiRun(runProcessInput, runProcessOutput); + } + + /////////////////////// + // map output values // + /////////////////////// + for(QFieldMetaData outputField : apiProcessMetaData.getOutputFields()) + { + output.put(outputField.getName(), runProcessOutput.getValues().get(outputField.getName())); + } + + return (output); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -1078,14 +1185,14 @@ public class ApiImplementation ApiTableMetaDataContainer apiTableMetaDataContainer = ApiTableMetaDataContainer.of(table); if(apiTableMetaDataContainer == null) { - LOG.info("404 because table apiMetaDataContainer is null", logPairs); + LOG.info("404 because table apiTableMetaDataContainer is null", logPairs); throw (new QNotFoundException("Could not find a table named " + tableApiName + " in this api.")); } ApiTableMetaData apiTableMetaData = apiTableMetaDataContainer.getApiTableMetaData(apiInstanceMetaData.getName()); if(apiTableMetaData == null) { - LOG.info("404 because table apiMetaData is null", logPairs); + LOG.info("404 because table apiTableMetaData is null", logPairs); throw (new QNotFoundException("Could not find a table named " + tableApiName + " in this api.")); } @@ -1126,6 +1233,65 @@ public class ApiImplementation + /******************************************************************************* + ** + *******************************************************************************/ + public static QProcessMetaData validateProcessAndVersion(ApiInstanceMetaData apiInstanceMetaData, String version, String processApiName) throws QNotFoundException + { + QProcessMetaData process = getProcessByApiName(apiInstanceMetaData.getName(), version, processApiName); + LogPair[] logPairs = new LogPair[] { logPair("apiName", apiInstanceMetaData.getName()), logPair("version", version), logPair("processApiName", processApiName) }; + + if(process == null) + { + LOG.info("404 because process is null (processApiName=" + processApiName + ")", logPairs); + throw (new QNotFoundException("Could not find a process named " + processApiName + " in this api.")); + } + + if(BooleanUtils.isTrue(process.getIsHidden())) + { + LOG.info("404 because process isHidden", logPairs); + throw (new QNotFoundException("Could not find a process named " + processApiName + " in this api.")); + } + + ApiProcessMetaDataContainer apiProcessMetaDataContainer = ApiProcessMetaDataContainer.of(process); + if(apiProcessMetaDataContainer == null) + { + LOG.info("404 because process apiProcessMetaDataContainer is null", logPairs); + throw (new QNotFoundException("Could not find a process named " + processApiName + " in this api.")); + } + + ApiProcessMetaData apiProcessMetaData = apiProcessMetaDataContainer.getApiProcessMetaData(apiInstanceMetaData.getName()); + if(apiProcessMetaData == null) + { + LOG.info("404 because process apiProcessMetaData is null", logPairs); + throw (new QNotFoundException("Could not find a process named " + processApiName + " in this api.")); + } + + if(BooleanUtils.isTrue(apiProcessMetaData.getIsExcluded())) + { + LOG.info("404 because process is excluded", logPairs); + throw (new QNotFoundException("Could not find a process named " + processApiName + " in this api.")); + } + + APIVersion requestApiVersion = new APIVersion(version); + List supportedVersions = apiInstanceMetaData.getSupportedVersions(); + if(CollectionUtils.nullSafeIsEmpty(supportedVersions) || !supportedVersions.contains(requestApiVersion)) + { + LOG.info("404 because requested version is not supported", logPairs); + throw (new QNotFoundException(version + " is not a supported version in this api.")); + } + + if(!apiProcessMetaData.getApiVersionRange().includes(requestApiVersion)) + { + LOG.info("404 because process version range does not include requested version", logPairs); + throw (new QNotFoundException(version + " is not a supported version for process " + processApiName + " in this api.")); + } + + return (process); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -1167,6 +1333,100 @@ public class ApiImplementation + /******************************************************************************* + ** + *******************************************************************************/ + private static QProcessMetaData getProcessByApiName(String apiName, String version, String processApiName) + { + ///////////////////////////////////////////////////////////////////////////////////////////// + // processApiNameMap is a map of (apiName,apiVersion) => Map. // + // that is to say, a 2-level map. The first level is keyed by (apiName,apiVersion) pairs. // + // the second level is keyed by processApiNames. // + ///////////////////////////////////////////////////////////////////////////////////////////// + Pair key = new Pair<>(apiName, version); + if(processApiNameMap.get(key) == null) + { + Map map = new HashMap<>(); + + for(QProcessMetaData process : QContext.getQInstance().getProcesses().values()) + { + ApiProcessMetaDataContainer apiProcessMetaDataContainer = ApiProcessMetaDataContainer.of(process); + if(apiProcessMetaDataContainer != null) + { + ApiProcessMetaData apiProcessMetaData = apiProcessMetaDataContainer.getApiProcessMetaData(apiName); + if(apiProcessMetaData != null) + { + String name = process.getName(); + if(StringUtils.hasContent(apiProcessMetaData.getApiProcessName())) + { + name = apiProcessMetaData.getApiProcessName(); + } + map.put(name, process); + } + } + } + + processApiNameMap.put(key, map); + } + + return (processApiNameMap.get(key).get(processApiName)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static ApiProcessMetaData getApiProcessMetaDataIfProcessIsInApi(ApiInstanceMetaData apiInstanceMetaData, QProcessMetaData process) + { + if(BooleanUtils.isTrue(process.getIsHidden())) + { + LOG.trace("excluding process because it is hidden (process=" + process.getName() + ")"); + return (null); + } + + ApiProcessMetaDataContainer apiProcessMetaDataContainer = ApiProcessMetaDataContainer.of(process); + if(apiProcessMetaDataContainer == null) + { + LOG.trace("excluding process because apiProcessMetaDataContainer is null (process=" + process.getName() + ")"); + return (null); + } + + ApiProcessMetaData apiProcessMetaData = apiProcessMetaDataContainer.getApiProcessMetaData(apiInstanceMetaData.getName()); + if(apiProcessMetaData == null) + { + LOG.trace("excluding process because apiProcessMetaData is null (process=" + process.getName() + ")"); + return (null); + } + + if(BooleanUtils.isTrue(apiProcessMetaData.getIsExcluded())) + { + LOG.trace("excluding process because is excluded (process=" + process.getName() + ")"); + return (null); + } + + boolean isProcessInAnySupportedVersions = false; + List supportedVersions = apiInstanceMetaData.getSupportedVersions(); + APIVersionRange apiVersionRange = apiProcessMetaData.getApiVersionRange(); + for(APIVersion supportedVersion : supportedVersions) + { + if(apiVersionRange.includes(supportedVersion)) + { + isProcessInAnySupportedVersions = true; + } + } + + if(!isProcessInAnySupportedVersions) + { + LOG.trace("excluding process because it is not in any supported versions (process=" + process.getName() + ")"); + return (null); + } + + return (apiProcessMetaData); + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandler.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandler.java index 98484dce..764884cc 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandler.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandler.java @@ -29,6 +29,7 @@ import java.time.Instant; import java.util.ArrayList; import java.util.Base64; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; @@ -43,8 +44,10 @@ import com.kingsrook.qqq.api.model.actions.GenerateOpenApiSpecOutput; import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaData; import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaDataContainer; import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaDataProvider; +import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessMetaData; import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaData; import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaDataContainer; +import com.kingsrook.qqq.api.model.openapi.HttpMethod; import com.kingsrook.qqq.backend.core.actions.tables.GetAction; import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; import com.kingsrook.qqq.backend.core.context.QContext; @@ -54,6 +57,7 @@ import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QModuleDispatchException; import com.kingsrook.qqq.backend.core.exceptions.QNotFoundException; import com.kingsrook.qqq.backend.core.exceptions.QPermissionDeniedException; +import com.kingsrook.qqq.backend.core.exceptions.QRuntimeException; import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException; import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput; @@ -66,6 +70,8 @@ import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.authentication.Auth0AuthenticationMetaData; import com.kingsrook.qqq.backend.core.model.metadata.branding.QBrandingMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; +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.model.session.QSession; import com.kingsrook.qqq.backend.core.model.session.QUser; @@ -156,10 +162,43 @@ public class QJavalinApiHandler //////////////////////////////////////////// ApiBuilder.get("/", context -> doSpecHtml(context, apiInstanceMetaData)); + /////////////////////////////////////////// + // add known paths for specs & docs page // + /////////////////////////////////////////// ApiBuilder.get("/openapi.yaml", context -> doSpecYaml(context, apiInstanceMetaData)); ApiBuilder.get("/openapi.json", context -> doSpecJson(context, apiInstanceMetaData)); ApiBuilder.get("/openapi.html", context -> doSpecHtml(context, apiInstanceMetaData)); + /////////////////// + // add processes // + /////////////////// + for(QProcessMetaData process : qInstance.getProcesses().values()) + { + ApiProcessMetaData apiProcessMetaData = ApiImplementation.getApiProcessMetaDataIfProcessIsInApi(apiInstanceMetaData, process); + if(apiProcessMetaData != null) + { + String path = getProcessApiPath(process, apiProcessMetaData, apiInstanceMetaData); + HttpMethod method = apiProcessMetaData.getMethod(); + switch(method) + { + case GET -> ApiBuilder.get(path, context -> runProcess(context, process, apiProcessMetaData, apiInstanceMetaData)); + case POST -> ApiBuilder.post(path, context -> runProcess(context, process, apiProcessMetaData, apiInstanceMetaData)); + case PUT -> ApiBuilder.put(path, context -> runProcess(context, process, apiProcessMetaData, apiInstanceMetaData)); + case PATCH -> ApiBuilder.patch(path, context -> runProcess(context, process, apiProcessMetaData, apiInstanceMetaData)); + case DELETE -> ApiBuilder.delete(path, context -> runProcess(context, process, apiProcessMetaData, apiInstanceMetaData)); + default -> throw (new QRuntimeException("Unrecognized http method [" + method + "] for process [" + process.getName() + "]")); + } + + if(doesProcessSupportAsync(apiInstanceMetaData, process)) + { + ApiBuilder.get(path + "/status/{processId}", context -> getProcessStatus(context, apiInstanceMetaData)); + } + } + } + + /////////////////////////////////// + // add wildcard paths for tables // + /////////////////////////////////// ApiBuilder.path("/{tableName}", () -> { ApiBuilder.get("/openapi.yaml", context -> doSpecYaml(context, apiInstanceMetaData)); @@ -208,6 +247,104 @@ public class QJavalinApiHandler + /******************************************************************************* + ** + *******************************************************************************/ + private void getProcessStatus(Context context, ApiInstanceMetaData apiInstanceMetaData) + { + // todo! + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @SuppressWarnings("checkstyle:indentation") + private void runProcess(Context context, QProcessMetaData processMetaData, ApiProcessMetaData apiProcessMetaData, ApiInstanceMetaData apiInstanceMetaData) + { + String version = context.pathParam("version"); + APILog apiLog = newAPILog(context); + + try + { + setupSession(context, null, version, apiInstanceMetaData); + QJavalinAccessLogger.logStart("apiRunProcess", logPair("process", processMetaData.getName())); + + Map parameters = new LinkedHashMap<>(); + for(QFieldMetaData inputField : CollectionUtils.nonNullList(apiProcessMetaData.getInputFields())) + { + String value = switch(apiProcessMetaData.getMethod()) + { + case GET -> context.queryParam(inputField.getName()); + // todo - other methods (all from a JSON body??) + default -> throw new QException("Http method " + apiLog.getMethod() + " is not yet implemented for reading parameters"); + }; + parameters.put(inputField.getName(), value); + } + + Map outputRecord = ApiImplementation.runProcess(apiInstanceMetaData, version, apiProcessMetaData.getApiProcessName(), parameters); + + QJavalinAccessLogger.logEndSuccess(); + String resultString = toJson(outputRecord); + context.result(resultString); + storeApiLog(apiLog.withStatusCode(context.statusCode()).withResponseBody(resultString)); + } + catch(Exception e) + { + QJavalinAccessLogger.logEndFail(e); + handleException(context, e, apiLog); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private boolean doesProcessSupportAsync(ApiInstanceMetaData apiInstanceMetaData, QProcessMetaData process) + { + // todo - implement + return false; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private String getProcessApiPath(QProcessMetaData process, ApiProcessMetaData apiProcessMetaData, ApiInstanceMetaData apiInstanceMetaData) + { + if(StringUtils.hasContent(apiProcessMetaData.getPath())) + { + return apiProcessMetaData.getPath() + "/" + apiProcessMetaData.getApiProcessName(); + } + else if(StringUtils.hasContent(process.getTableName())) + { + QTableMetaData table = qInstance.getTable(process.getTableName()); + String tablePathPart = table.getName(); + ApiTableMetaDataContainer apiTableMetaDataContainer = ApiTableMetaDataContainer.of(table); + if(apiTableMetaDataContainer != null) + { + ApiTableMetaData apiTableMetaData = apiTableMetaDataContainer.getApis().get(apiInstanceMetaData.getName()); + if(apiTableMetaData != null) + { + if(StringUtils.hasContent(apiTableMetaData.getApiTableName())) + { + tablePathPart = apiTableMetaData.getApiTableName(); + } + } + } + return tablePathPart + "/" + apiProcessMetaData.getApiProcessName(); + } + else + { + return apiProcessMetaData.getApiProcessName(); + } + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessCustomizers.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessCustomizers.java new file mode 100644 index 00000000..daf01f93 --- /dev/null +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessCustomizers.java @@ -0,0 +1,87 @@ +/* + * 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.api.model.metadata.processes; + + +/******************************************************************************* + ** + *******************************************************************************/ +public enum ApiProcessCustomizers +{ + PRE_RUN("preRun", PreRunApiProcessCustomizer.class), + POST_RUN("postRun", PreRunApiProcessCustomizer.class); + + private final String role; + private final Class expectedType; + + + + /******************************************************************************* + ** + *******************************************************************************/ + ApiProcessCustomizers(String role, Class expectedType) + { + this.role = role; + this.expectedType = expectedType; + } + + + + /******************************************************************************* + ** Get the FilesystemTableCustomer for a given role (e.g., the role used in meta-data, not + ** the enum-constant name). + *******************************************************************************/ + public static ApiProcessCustomizers forRole(String name) + { + for(ApiProcessCustomizers value : values()) + { + if(value.role.equals(name)) + { + return (value); + } + } + + return (null); + } + + + + /******************************************************************************* + ** Getter for role + ** + *******************************************************************************/ + public String getRole() + { + return role; + } + + + + /******************************************************************************* + ** Getter for expectedType + ** + *******************************************************************************/ + public Class getExpectedType() + { + return expectedType; + } +} diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessMetaData.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessMetaData.java new file mode 100644 index 00000000..d3920a95 --- /dev/null +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessMetaData.java @@ -0,0 +1,496 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. 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.api.model.metadata.processes; + + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import com.kingsrook.qqq.api.ApiSupplementType; +import com.kingsrook.qqq.api.model.APIVersionRange; +import com.kingsrook.qqq.api.model.metadata.fields.ApiFieldMetaData; +import com.kingsrook.qqq.api.model.metadata.fields.ApiFieldMetaDataContainer; +import com.kingsrook.qqq.api.model.openapi.HttpMethod; +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.processes.QFrontendStepMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; +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; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class ApiProcessMetaData +{ + private String initialVersion; + private String finalVersion; + + private String apiProcessName; + private Boolean isExcluded; + + private String path; + private HttpMethod method; + + private List inputFields; + private List outputFields; + + private Map customizers; + + + + /******************************************************************************* + ** + *******************************************************************************/ + public ApiProcessMetaData withInferredInputFields(QProcessMetaData processMetaData) + { + inputFields = new ArrayList<>(); + for(QStepMetaData stepMetaData : CollectionUtils.nonNullList(processMetaData.getStepList())) + { + if(stepMetaData instanceof QFrontendStepMetaData frontendStep) + { + inputFields.addAll(frontendStep.getInputFields()); + } + } + + return (this); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public ApiProcessMetaData withInferredOutputFields(QProcessMetaData processMetaData) + { + outputFields = new ArrayList<>(); + for(QStepMetaData stepMetaData : CollectionUtils.nonNullList(processMetaData.getStepList())) + { + if(stepMetaData instanceof QFrontendStepMetaData frontendStep) + { + outputFields.addAll(frontendStep.getOutputFields()); + } + } + + return (this); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public APIVersionRange getApiVersionRange() + { + if(getInitialVersion() == null) + { + return APIVersionRange.none(); + } + + return (getFinalVersion() != null + ? APIVersionRange.betweenAndIncluding(getInitialVersion(), getFinalVersion()) + : APIVersionRange.afterAndIncluding(getInitialVersion())); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @SuppressWarnings("unchecked") + public void enrich(String apiName, QProcessMetaData process) + { + if(!StringUtils.hasContent(getApiProcessName())) + { + setApiProcessName(process.getName()); + } + + if(initialVersion != null) + { + /////////////////////////////////////////////////////////////// + // make sure all fields have at least an initial version set // + /////////////////////////////////////////////////////////////// + for(QFieldMetaData field : CollectionUtils.mergeLists(getInputFields(), getOutputFields())) + { + ApiFieldMetaData apiFieldMetaData = ensureFieldHasApiSupplementalMetaData(apiName, field); + if(apiFieldMetaData.getInitialVersion() == null) + { + apiFieldMetaData.setInitialVersion(initialVersion); + } + } + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static ApiFieldMetaData ensureFieldHasApiSupplementalMetaData(String apiName, QFieldMetaData field) + { + if(field.getSupplementalMetaData(ApiSupplementType.NAME) == null) + { + field.withSupplementalMetaData(new ApiFieldMetaDataContainer()); + } + + ApiFieldMetaDataContainer apiFieldMetaDataContainer = ApiFieldMetaDataContainer.of(field); + if(apiFieldMetaDataContainer.getApiFieldMetaData(apiName) == null) + { + apiFieldMetaDataContainer.withApiFieldMetaData(apiName, new ApiFieldMetaData()); + } + + return (apiFieldMetaDataContainer.getApiFieldMetaData(apiName)); + } + + + + /******************************************************************************* + ** Fluent setter for a single outputField + *******************************************************************************/ + public ApiProcessMetaData withOutputField(QFieldMetaData outputField) + { + if(this.outputFields == null) + { + this.outputFields = new ArrayList<>(); + } + this.outputFields.add(outputField); + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for a single inputField + *******************************************************************************/ + public ApiProcessMetaData withInputField(QFieldMetaData inputField) + { + if(this.inputFields == null) + { + this.inputFields = new ArrayList<>(); + } + this.inputFields.add(inputField); + return (this); + } + + + + /******************************************************************************* + ** Getter for initialVersion + *******************************************************************************/ + public String getInitialVersion() + { + return (this.initialVersion); + } + + + + /******************************************************************************* + ** Setter for initialVersion + *******************************************************************************/ + public void setInitialVersion(String initialVersion) + { + this.initialVersion = initialVersion; + } + + + + /******************************************************************************* + ** Fluent setter for initialVersion + *******************************************************************************/ + public ApiProcessMetaData withInitialVersion(String initialVersion) + { + this.initialVersion = initialVersion; + return (this); + } + + + + /******************************************************************************* + ** Getter for finalVersion + *******************************************************************************/ + public String getFinalVersion() + { + return (this.finalVersion); + } + + + + /******************************************************************************* + ** Setter for finalVersion + *******************************************************************************/ + public void setFinalVersion(String finalVersion) + { + this.finalVersion = finalVersion; + } + + + + /******************************************************************************* + ** Fluent setter for finalVersion + *******************************************************************************/ + public ApiProcessMetaData withFinalVersion(String finalVersion) + { + this.finalVersion = finalVersion; + return (this); + } + + + + /******************************************************************************* + ** Getter for apiProcessName + *******************************************************************************/ + public String getApiProcessName() + { + return (this.apiProcessName); + } + + + + /******************************************************************************* + ** Setter for apiProcessName + *******************************************************************************/ + public void setApiProcessName(String apiProcessName) + { + this.apiProcessName = apiProcessName; + } + + + + /******************************************************************************* + ** Fluent setter for apiProcessName + *******************************************************************************/ + public ApiProcessMetaData withApiProcessName(String apiProcessName) + { + this.apiProcessName = apiProcessName; + return (this); + } + + + + /******************************************************************************* + ** Getter for isExcluded + *******************************************************************************/ + public Boolean getIsExcluded() + { + return (this.isExcluded); + } + + + + /******************************************************************************* + ** Setter for isExcluded + *******************************************************************************/ + public void setIsExcluded(Boolean isExcluded) + { + this.isExcluded = isExcluded; + } + + + + /******************************************************************************* + ** Fluent setter for isExcluded + *******************************************************************************/ + public ApiProcessMetaData withIsExcluded(Boolean isExcluded) + { + this.isExcluded = isExcluded; + return (this); + } + + + + /******************************************************************************* + ** Getter for method + *******************************************************************************/ + public HttpMethod getMethod() + { + return (this.method); + } + + + + /******************************************************************************* + ** Setter for method + *******************************************************************************/ + public void setMethod(HttpMethod method) + { + this.method = method; + } + + + + /******************************************************************************* + ** Fluent setter for method + *******************************************************************************/ + public ApiProcessMetaData withMethod(HttpMethod method) + { + this.method = method; + return (this); + } + + + + /******************************************************************************* + ** Getter for path + *******************************************************************************/ + public String getPath() + { + return (this.path); + } + + + + /******************************************************************************* + ** Setter for path + *******************************************************************************/ + public void setPath(String path) + { + this.path = path; + } + + + + /******************************************************************************* + ** Fluent setter for path + *******************************************************************************/ + public ApiProcessMetaData withPath(String path) + { + this.path = path; + return (this); + } + + + + /******************************************************************************* + ** Getter for inputFields + *******************************************************************************/ + public List getInputFields() + { + return (this.inputFields); + } + + + + /******************************************************************************* + ** Setter for inputFields + *******************************************************************************/ + public void setInputFields(List inputFields) + { + this.inputFields = inputFields; + } + + + + /******************************************************************************* + ** Fluent setter for inputFields + *******************************************************************************/ + public ApiProcessMetaData withInputFields(List inputFields) + { + this.inputFields = inputFields; + return (this); + } + + + + /******************************************************************************* + ** Getter for outputFields + *******************************************************************************/ + public List getOutputFields() + { + return (this.outputFields); + } + + + + /******************************************************************************* + ** Setter for outputFields + *******************************************************************************/ + public void setOutputFields(List outputFields) + { + this.outputFields = outputFields; + } + + + + /******************************************************************************* + ** Fluent setter for outputFields + *******************************************************************************/ + public ApiProcessMetaData withOutputFields(List outputFields) + { + this.outputFields = outputFields; + return (this); + } + + + + /******************************************************************************* + ** Getter for customizers + *******************************************************************************/ + public Map getCustomizers() + { + return (this.customizers); + } + + + + /******************************************************************************* + ** Setter for customizers + *******************************************************************************/ + public void setCustomizers(Map customizers) + { + this.customizers = customizers; + } + + + + /******************************************************************************* + ** Fluent setter for customizers + *******************************************************************************/ + public ApiProcessMetaData withCustomizers(Map customizers) + { + this.customizers = customizers; + return (this); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public ApiProcessMetaData withCustomizer(String role, QCodeReference customizer) + { + if(this.customizers == null) + { + this.customizers = new HashMap<>(); + } + + if(this.customizers.containsKey(role)) + { + throw (new IllegalArgumentException("Attempt to add a second customizer with role [" + role + "] to apiProcess [" + apiProcessName + "].")); + } + this.customizers.put(role, customizer); + return (this); + } + +} diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessMetaDataContainer.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessMetaDataContainer.java new file mode 100644 index 00000000..36a2b352 --- /dev/null +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessMetaDataContainer.java @@ -0,0 +1,138 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. 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.api.model.metadata.processes; + + +import java.util.LinkedHashMap; +import java.util.Map; +import com.kingsrook.qqq.api.ApiSupplementType; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QSupplementalProcessMetaData; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class ApiProcessMetaDataContainer extends QSupplementalProcessMetaData +{ + private Map apis; + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public ApiProcessMetaDataContainer() + { + setType("api"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static ApiProcessMetaDataContainer of(QProcessMetaData process) + { + return ((ApiProcessMetaDataContainer) process.getSupplementalMetaData(ApiSupplementType.NAME)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void enrich(QProcessMetaData process) + { + super.enrich(process); + + for(Map.Entry entry : CollectionUtils.nonNullMap(apis).entrySet()) + { + entry.getValue().enrich(entry.getKey(), process); + } + } + + + + /******************************************************************************* + ** Getter for apis + *******************************************************************************/ + public Map getApis() + { + return (this.apis); + } + + + + /******************************************************************************* + ** Getter for apis + *******************************************************************************/ + public ApiProcessMetaData getApiProcessMetaData(String apiName) + { + if(this.apis == null) + { + return (null); + } + + return (this.apis.get(apiName)); + } + + + + /******************************************************************************* + ** Setter for apis + *******************************************************************************/ + public void setApis(Map apis) + { + this.apis = apis; + } + + + + /******************************************************************************* + ** Fluent setter for apis + *******************************************************************************/ + public ApiProcessMetaDataContainer withApis(Map apis) + { + this.apis = apis; + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for apis + *******************************************************************************/ + public ApiProcessMetaDataContainer withApiProcessMetaData(String apiName, ApiProcessMetaData apiProcessMetaData) + { + if(this.apis == null) + { + this.apis = new LinkedHashMap<>(); + } + this.apis.put(apiName, apiProcessMetaData); + return (this); + } + +} diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/PostRunApiProcessCustomizer.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/PostRunApiProcessCustomizer.java new file mode 100644 index 00000000..06ba64f8 --- /dev/null +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/PostRunApiProcessCustomizer.java @@ -0,0 +1,20 @@ +package com.kingsrook.qqq.api.model.metadata.processes; + + +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput; + + +/******************************************************************************* + ** + *******************************************************************************/ +public interface PostRunApiProcessCustomizer +{ + + /******************************************************************************* + ** + *******************************************************************************/ + void postApiRun(RunProcessInput runProcessInput, RunProcessOutput runProcessOutput) throws QException; + +} diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/PreRunApiProcessCustomizer.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/PreRunApiProcessCustomizer.java new file mode 100644 index 00000000..e1836708 --- /dev/null +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/PreRunApiProcessCustomizer.java @@ -0,0 +1,19 @@ +package com.kingsrook.qqq.api.model.metadata.processes; + + +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput; + + +/******************************************************************************* + ** + *******************************************************************************/ +public interface PreRunApiProcessCustomizer +{ + + /******************************************************************************* + ** + *******************************************************************************/ + void preApiRun(RunProcessInput runProcessInput) throws QException; + +} diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/HttpMethod.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/HttpMethod.java new file mode 100644 index 00000000..c04d577f --- /dev/null +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/HttpMethod.java @@ -0,0 +1,14 @@ +package com.kingsrook.qqq.api.model.openapi; + + +/******************************************************************************* + ** + *******************************************************************************/ +public enum HttpMethod +{ + GET, + POST, + PUT, + PATCH, + DELETE +} diff --git a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/GetPersonInfoStep.java b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/GetPersonInfoStep.java new file mode 100644 index 00000000..96191b58 --- /dev/null +++ b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/GetPersonInfoStep.java @@ -0,0 +1,27 @@ +package com.kingsrook.qqq.api; + + +import java.math.BigDecimal; +import com.kingsrook.qqq.backend.core.actions.processes.BackendStep; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class GetPersonInfoStep implements BackendStep +{ + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException + { + runBackendStepOutput.addValue("density", new BigDecimal("3.50")); + runBackendStepOutput.addValue("daysOld", runBackendStepInput.getValueInteger("age") * 365); + runBackendStepOutput.addValue("nickname", "Guy from " + runBackendStepInput.getValueString("homeTown")); + } + +} diff --git a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/TestUtils.java b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/TestUtils.java index 48b0eade..fdef3cd5 100644 --- a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/TestUtils.java +++ b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/TestUtils.java @@ -29,8 +29,11 @@ import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaData; import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaDataContainer; import com.kingsrook.qqq.api.model.metadata.fields.ApiFieldMetaData; import com.kingsrook.qqq.api.model.metadata.fields.ApiFieldMetaDataContainer; +import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessMetaData; +import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessMetaDataContainer; import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaData; import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaDataContainer; +import com.kingsrook.qqq.api.model.openapi.HttpMethod; import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPreDeleteCustomizer; import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPreInsertCustomizer; import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPreUpdateCustomizer; @@ -45,12 +48,23 @@ 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.authentication.Auth0AuthenticationMetaData; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; +import com.kingsrook.qqq.backend.core.model.metadata.dashboard.nocode.HtmlWrapper; +import com.kingsrook.qqq.backend.core.model.metadata.dashboard.nocode.WidgetHtmlLine; import com.kingsrook.qqq.backend.core.model.metadata.fields.DisplayFormat; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn; import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinType; import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.PVSValueFormatAndFields; +import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource; +import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType; +import com.kingsrook.qqq.backend.core.model.metadata.processes.NoCodeWidgetFrontendComponentMetaData; +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; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.Association; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey; @@ -59,6 +73,7 @@ import com.kingsrook.qqq.backend.core.model.statusmessages.QWarningMessage; import com.kingsrook.qqq.backend.core.model.statusmessages.SystemErrorStatusMessage; import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryBackendModule; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import com.kingsrook.qqq.backend.core.utils.collections.ListBuilder; /******************************************************************************* @@ -74,6 +89,8 @@ public class TestUtils public static final String TABLE_NAME_LINE_ITEM_EXTRINSIC = "orderLineExtrinsic"; public static final String TABLE_NAME_ORDER_EXTRINSIC = "orderExtrinsic"; + public static final String PROCESS_NAME_GET_PERSON_INFO = "getPersonInfo"; + public static final String API_NAME = "test-api"; public static final String ALTERNATIVE_API_NAME = "person-api"; @@ -103,6 +120,9 @@ public class TestUtils qInstance.addJoin(defineJoinLineItemLineItemExtrinsic()); qInstance.addJoin(defineJoinOrderOrderExtrinsic()); + qInstance.addPossibleValueSource(definePersonPossibleValueSource()); + qInstance.addProcess(defineProcessGetPersonInfo()); + qInstance.setAuthentication(new Auth0AuthenticationMetaData().withType(QAuthenticationType.FULLY_ANONYMOUS).withName("anonymous")); qInstance.withSupplementalMetaData(new ApiInstanceMetaDataContainer() @@ -133,6 +153,80 @@ public class TestUtils + /******************************************************************************* + ** + *******************************************************************************/ + private static QPossibleValueSource definePersonPossibleValueSource() + { + return new QPossibleValueSource() + .withName(TABLE_NAME_PERSON) + .withType(QPossibleValueSourceType.TABLE) + .withTableName(TABLE_NAME_PERSON) + .withValueFormatAndFields(PVSValueFormatAndFields.LABEL_ONLY); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static QProcessMetaData defineProcessGetPersonInfo() + { + QProcessMetaData process = new QProcessMetaData() + .withName(PROCESS_NAME_GET_PERSON_INFO) + .withLabel("Get Person Info") + .withTableName(TABLE_NAME_PERSON) + .addStep(new QFrontendStepMetaData() + .withName("enterInputs") + .withLabel("Person Info Input") + .withComponent(new QFrontendComponentMetaData().withType(QComponentType.EDIT_FORM)) + + .withFormField(new QFieldMetaData("age", QFieldType.INTEGER)) + .withFormField(new QFieldMetaData("partnerPersonId", QFieldType.INTEGER).withPossibleValueSourceName(TABLE_NAME_PERSON)) + .withFormField(new QFieldMetaData("heightInches", QFieldType.DECIMAL)) + .withFormField(new QFieldMetaData("weightPounds", QFieldType.INTEGER)) + .withFormField(new QFieldMetaData("homeTown", QFieldType.STRING)) + + .withComponent(new NoCodeWidgetFrontendComponentMetaData() + + .withOutput(new WidgetHtmlLine() + .withWrapper(HtmlWrapper.divWithStyles(HtmlWrapper.STYLE_FLOAT_RIGHT, HtmlWrapper.STYLE_MEDIUM_CENTERED, HtmlWrapper.styleWidth("50%"))) + .withVelocityTemplate(""" + Density:
$density
+ """)) + + .withOutput(new WidgetHtmlLine() + .withVelocityTemplate(""" + Days old: $daysOld
+ Nickname: $nickname
+ """)) + )) + + .addStep(new QBackendStepMetaData() + .withName("execute") + .withCode(new QCodeReference(GetPersonInfoStep.class))) + + .addStep(new QFrontendStepMetaData() + .withName("dummyStep") + ); + + process.withSupplementalMetaData(new ApiProcessMetaDataContainer() + .withApiProcessMetaData(API_NAME, new ApiProcessMetaData() + .withInitialVersion(CURRENT_API_VERSION) + .withMethod(HttpMethod.GET) + .withInferredInputFields(process) + .withOutputFields(ListBuilder.of( + new QFieldMetaData("density", QFieldType.DECIMAL), + new QFieldMetaData("daysOld", QFieldType.INTEGER), + new QFieldMetaData("nickname", QFieldType.STRING) + )) + )); + + return (process); + } + + + /******************************************************************************* ** Define the in-memory backend used in standard tests *******************************************************************************/ diff --git a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandlerTest.java b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandlerTest.java index 6a274cc2..b7889b1d 100644 --- a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandlerTest.java +++ b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandlerTest.java @@ -1439,6 +1439,20 @@ class QJavalinApiHandlerTest extends BaseTest + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testProcess() throws QException + { + HttpResponse response = Unirest.get(BASE_URL + "/api/" + VERSION + "/person/getPersonInfo?age=43&partnerPersonId=1&heightInches=72&weightPounds=220&homeTown=Chester").asString(); + assertEquals(HttpStatus.OK_200, response.getStatus()); + JSONObject jsonObject = new JSONObject(response.getBody()); + System.out.println(jsonObject.toString(3)); + } + + + /******************************************************************************* ** *******************************************************************************/