Initial implementation of api processes

This commit is contained in:
2023-06-12 10:58:30 -05:00
parent 6a01754479
commit a340299c67
16 changed files with 1644 additions and 8 deletions

View File

@ -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);
}

View File

@ -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)));
}

View File

@ -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
**

View File

@ -61,6 +61,7 @@ public class QProcessMetaData implements QAppChildMetaData, MetaDataWithPermissi
private QScheduleMetaData schedule;
private Map<String, QSupplementalProcessMetaData> supplementalMetaData;
/*******************************************************************************
@ -544,4 +545,64 @@ public class QProcessMetaData implements QAppChildMetaData, MetaDataWithPermissi
qInstance.addProcess(this);
}
/*******************************************************************************
** Getter for supplementalMetaData
*******************************************************************************/
public Map<String, QSupplementalProcessMetaData> 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<String, QSupplementalProcessMetaData> supplementalMetaData)
{
this.supplementalMetaData = supplementalMetaData;
}
/*******************************************************************************
** Fluent setter for supplementalMetaData
*******************************************************************************/
public QProcessMetaData withSupplementalMetaData(Map<String, QSupplementalProcessMetaData> 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);
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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 //
////////////////////////
}
}

View File

@ -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<apiName, apiVersion> //
/////////////////////////////////////
///////////////////////////////////////////////////////////////////
// key: Pair<apiName, apiVersion>, value: Map<name => metaData> //
///////////////////////////////////////////////////////////////////
private static Map<Pair<String, String>, Map<String, QTableMetaData>> tableApiNameMap = new HashMap<>();
private static Map<Pair<String, String>, Map<String, QProcessMetaData>> processApiNameMap = new HashMap<>();
@ -896,6 +910,99 @@ public class ApiImplementation
/*******************************************************************************
**
*******************************************************************************/
public static Map<String, Serializable> runProcess(ApiInstanceMetaData apiInstanceMetaData, String version, String processApiName, Map<String, String> paramMap) throws QException
{
QProcessMetaData process = validateProcessAndVersion(apiInstanceMetaData, version, processApiName);
String processName = process.getName();
ApiProcessMetaData apiProcessMetaData = getApiProcessMetaDataIfProcessIsInApi(apiInstanceMetaData, process);
List<String> badRequestMessages = new ArrayList<>();
Map<String, Serializable> 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<String, QCodeReference> 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<APIVersion> 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<String, QProcessMetaData>. //
// 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<String, String> key = new Pair<>(apiName, version);
if(processApiNameMap.get(key) == null)
{
Map<String, QProcessMetaData> 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<APIVersion> 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);
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -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<String, String> 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<String, Serializable> 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();
}
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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;
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<QFieldMetaData> inputFields;
private List<QFieldMetaData> outputFields;
private Map<String, QCodeReference> 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<QFieldMetaData> getInputFields()
{
return (this.inputFields);
}
/*******************************************************************************
** Setter for inputFields
*******************************************************************************/
public void setInputFields(List<QFieldMetaData> inputFields)
{
this.inputFields = inputFields;
}
/*******************************************************************************
** Fluent setter for inputFields
*******************************************************************************/
public ApiProcessMetaData withInputFields(List<QFieldMetaData> inputFields)
{
this.inputFields = inputFields;
return (this);
}
/*******************************************************************************
** Getter for outputFields
*******************************************************************************/
public List<QFieldMetaData> getOutputFields()
{
return (this.outputFields);
}
/*******************************************************************************
** Setter for outputFields
*******************************************************************************/
public void setOutputFields(List<QFieldMetaData> outputFields)
{
this.outputFields = outputFields;
}
/*******************************************************************************
** Fluent setter for outputFields
*******************************************************************************/
public ApiProcessMetaData withOutputFields(List<QFieldMetaData> outputFields)
{
this.outputFields = outputFields;
return (this);
}
/*******************************************************************************
** Getter for customizers
*******************************************************************************/
public Map<String, QCodeReference> getCustomizers()
{
return (this.customizers);
}
/*******************************************************************************
** Setter for customizers
*******************************************************************************/
public void setCustomizers(Map<String, QCodeReference> customizers)
{
this.customizers = customizers;
}
/*******************************************************************************
** Fluent setter for customizers
*******************************************************************************/
public ApiProcessMetaData withCustomizers(Map<String, QCodeReference> 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);
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<String, ApiProcessMetaData> 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<String, ApiProcessMetaData> entry : CollectionUtils.nonNullMap(apis).entrySet())
{
entry.getValue().enrich(entry.getKey(), process);
}
}
/*******************************************************************************
** Getter for apis
*******************************************************************************/
public Map<String, ApiProcessMetaData> 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<String, ApiProcessMetaData> apis)
{
this.apis = apis;
}
/*******************************************************************************
** Fluent setter for apis
*******************************************************************************/
public ApiProcessMetaDataContainer withApis(Map<String, ApiProcessMetaData> 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);
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -0,0 +1,14 @@
package com.kingsrook.qqq.api.model.openapi;
/*******************************************************************************
**
*******************************************************************************/
public enum HttpMethod
{
GET,
POST,
PUT,
PATCH,
DELETE
}

View File

@ -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"));
}
}

View File

@ -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("""
<b>Density:</b><br />$density<br/>
"""))
.withOutput(new WidgetHtmlLine()
.withVelocityTemplate("""
<b>Days old:</b> $daysOld<br/>
<b>Nickname:</b> $nickname<br/>
"""))
))
.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
*******************************************************************************/

View File

@ -1439,6 +1439,20 @@ class QJavalinApiHandlerTest extends BaseTest
/*******************************************************************************
**
*******************************************************************************/
@Test
void testProcess() throws QException
{
HttpResponse<String> 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));
}
/*******************************************************************************
**
*******************************************************************************/