Checkpoint, WIP on processes in api

This commit is contained in:
2023-06-12 18:19:48 -05:00
parent a340299c67
commit eee7354e77
24 changed files with 1571 additions and 396 deletions

View File

@ -370,7 +370,7 @@ public class QInstanceEnricher
for(QSupplementalProcessMetaData supplementalProcessMetaData : CollectionUtils.nonNullMap(process.getSupplementalMetaData()).values())
{
supplementalProcessMetaData.enrich(process);
supplementalProcessMetaData.enrich(this, process);
}
enrichPermissionRules(process);

View File

@ -54,6 +54,9 @@ public class QProcessMetaData implements QAppChildMetaData, MetaDataWithPermissi
private BasepullConfiguration basepullConfiguration;
private QPermissionRules permissionRules;
private Integer minInputRecords = null;
private Integer maxInputRecords = null;
private List<QStepMetaData> stepList; // these are the steps that are ran, by-default, in the order they are ran in
private Map<String, QStepMetaData> steps; // this is the full map of possible steps
@ -64,6 +67,7 @@ public class QProcessMetaData implements QAppChildMetaData, MetaDataWithPermissi
private Map<String, QSupplementalProcessMetaData> supplementalMetaData;
/*******************************************************************************
**
*******************************************************************************/
@ -605,4 +609,66 @@ public class QProcessMetaData implements QAppChildMetaData, MetaDataWithPermissi
return (this);
}
/*******************************************************************************
** Getter for minInputRecords
*******************************************************************************/
public Integer getMinInputRecords()
{
return (this.minInputRecords);
}
/*******************************************************************************
** Setter for minInputRecords
*******************************************************************************/
public void setMinInputRecords(Integer minInputRecords)
{
this.minInputRecords = minInputRecords;
}
/*******************************************************************************
** Fluent setter for minInputRecords
*******************************************************************************/
public QProcessMetaData withMinInputRecords(Integer minInputRecords)
{
this.minInputRecords = minInputRecords;
return (this);
}
/*******************************************************************************
** Getter for maxInputRecords
*******************************************************************************/
public Integer getMaxInputRecords()
{
return (this.maxInputRecords);
}
/*******************************************************************************
** Setter for maxInputRecords
*******************************************************************************/
public void setMaxInputRecords(Integer maxInputRecords)
{
this.maxInputRecords = maxInputRecords;
}
/*******************************************************************************
** Fluent setter for maxInputRecords
*******************************************************************************/
public QProcessMetaData withMaxInputRecords(Integer maxInputRecords)
{
this.maxInputRecords = maxInputRecords;
return (this);
}
}

View File

@ -22,7 +22,7 @@
package com.kingsrook.qqq.backend.core.model.metadata.processes;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.instances.QInstanceEnricher;
/*******************************************************************************
@ -69,7 +69,7 @@ public abstract class QSupplementalProcessMetaData
/*******************************************************************************
**
*******************************************************************************/
public void enrich(QProcessMetaData process)
public void enrich(QInstanceEnricher qInstanceEnricher, QProcessMetaData process)
{
////////////////////////
// noop in base class //

View File

@ -0,0 +1,19 @@
package com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend;
import com.kingsrook.qqq.backend.core.exceptions.QException;
/*******************************************************************************
**
*******************************************************************************/
public class CouldNotFindQueryFilterForExtractStepException extends QException
{
/*******************************************************************************
**
*******************************************************************************/
public CouldNotFindQueryFilterForExtractStepException(String message)
{
super(message);
}
}

View File

@ -223,7 +223,7 @@ public class ExtractViaQueryStep extends AbstractExtractStep
return (new QQueryFilter().withCriteria(new QFilterCriteria(table.getPrimaryKeyField(), QCriteriaOperator.IN, idStrings)));
}
throw (new QException("Could not find query filter for Extract step."));
throw (new CouldNotFindQueryFilterForExtractStepException("Could not find query filter for Extract step."));
}

View File

@ -407,6 +407,30 @@ public class StreamedETLWithFrontendProcess
/*******************************************************************************
** Fluent setter for minInputRecords
**
*******************************************************************************/
public Builder withMinInputRecords(Integer minInputRecords)
{
processMetaData.setMinInputRecords(minInputRecords);
return (this);
}
/*******************************************************************************
** Fluent setter for maxInputRecords
**
*******************************************************************************/
public Builder withMaxInputRecords(Integer maxInputRecords)
{
processMetaData.setMaxInputRecords(maxInputRecords);
return (this);
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -22,6 +22,9 @@
package com.kingsrook.qqq.backend.core.utils;
import java.util.function.Consumer;
import java.util.function.Predicate;
import com.kingsrook.qqq.backend.core.utils.lambdas.UnsafeConsumer;
import com.kingsrook.qqq.backend.core.utils.lambdas.UnsafeSupplier;
@ -96,4 +99,44 @@ public class ObjectUtils
return (defaultIfThrew);
}
/*******************************************************************************
**
*******************************************************************************/
public static <T> void ifNotNull(T object, Consumer<T> consumer)
{
if(object != null)
{
consumer.accept(object);
}
}
/*******************************************************************************
**
*******************************************************************************/
public static <T, E extends Exception> void ifNotNullUnsafe(T object, UnsafeConsumer<T, E> consumer) throws E
{
if(object != null)
{
consumer.run(object);
}
}
/*******************************************************************************
**
*******************************************************************************/
public static <T> T requireConditionElse(T a, Predicate<T> condition, T b)
{
if(condition.test(a))
{
return (a);
}
return (b);
}
}

View File

@ -35,18 +35,18 @@ import java.util.function.Supplier;
**
** Can use it 2 ways:
** MapBuilder.of(key, value, key2, value2, ...) => Map (a HashMap)
** MapBuilder.<KeyType ValueType>of(SomeMap::new).with(key, value).with(key2, value2)...build() => SomeMap (the type you specify)
** MapBuilder.of(() -> new SomeMap<SomeKeyType, SomeValueType>()).with(key, value).with(key2, value2)...build() => SomeMap (the type you specify)
*******************************************************************************/
public class MapBuilder<K, V>
public class MapBuilder<K, V, M extends Map<K, V>>
{
private Map<K, V> map;
private M map;
/*******************************************************************************
**
*******************************************************************************/
private MapBuilder(Map<K, V> map)
private MapBuilder(M map)
{
this.map = map;
}
@ -56,7 +56,7 @@ public class MapBuilder<K, V>
/*******************************************************************************
**
*******************************************************************************/
public static <K, V> MapBuilder<K, V> of(Supplier<Map<K, V>> mapSupplier)
public static <K, V, M extends Map<K, V>> MapBuilder<K, V, M> of(Supplier<M> mapSupplier)
{
return (new MapBuilder<>(mapSupplier.get()));
}
@ -66,7 +66,7 @@ public class MapBuilder<K, V>
/*******************************************************************************
**
*******************************************************************************/
public MapBuilder<K, V> with(K key, V value)
public MapBuilder<K, V, M> with(K key, V value)
{
map.put(key, value);
return (this);
@ -77,7 +77,7 @@ public class MapBuilder<K, V>
/*******************************************************************************
**
*******************************************************************************/
public Map<K, V> build()
public M build()
{
return (this.map);
}

View File

@ -78,7 +78,7 @@ class MapBuilderTest
@Test
void testTypeYouRequest()
{
Map<String, Integer> myTreeMap = MapBuilder.<String, Integer>of(TreeMap::new).with("1", 1).with("2", 2).build();
TreeMap<String, Integer> myTreeMap = MapBuilder.of(() -> new TreeMap<String, Integer>()).with("1", 1).with("2", 2).build();
assertTrue(myTreeMap instanceof TreeMap);
}

View File

@ -25,6 +25,7 @@ package com.kingsrook.qqq.api.actions;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
@ -33,12 +34,15 @@ 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.actions.HttpApiResponse;
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.ApiProcessInput;
import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessInputFieldsContainer;
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.ApiProcessOutputInterface;
import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessUtils;
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;
@ -46,6 +50,7 @@ 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.QProcessCallback;
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;
@ -89,6 +94,7 @@ import com.kingsrook.qqq.backend.core.model.statusmessages.PermissionDeniedMessa
import com.kingsrook.qqq.backend.core.model.statusmessages.QErrorMessage;
import com.kingsrook.qqq.backend.core.model.statusmessages.QStatusMessage;
import com.kingsrook.qqq.backend.core.model.statusmessages.QWarningMessage;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.CouldNotFindQueryFilterForExtractStepException;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.Pair;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
@ -100,6 +106,7 @@ import org.json.JSONArray;
import org.json.JSONObject;
import org.json.JSONTokener;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
import static com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator.IN;
/*******************************************************************************
@ -113,7 +120,6 @@ public class ApiImplementation
// 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<>();
@ -913,14 +919,15 @@ public class ApiImplementation
/*******************************************************************************
**
*******************************************************************************/
public static Map<String, Serializable> runProcess(ApiInstanceMetaData apiInstanceMetaData, String version, String processApiName, Map<String, String> paramMap) throws QException
public static HttpApiResponse 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);
Pair<ApiProcessMetaData, QProcessMetaData> pair = ApiProcessUtils.getProcessMetaDataPair(apiInstanceMetaData, version, processApiName);
List<String> badRequestMessages = new ArrayList<>();
Map<String, Serializable> output = new LinkedHashMap<>();
ApiProcessMetaData apiProcessMetaData = pair.getA();
QProcessMetaData process = pair.getB();
String processName = process.getName();
List<String> badRequestMessages = new ArrayList<>();
String processUUID = UUID.randomUUID().toString();
@ -928,27 +935,37 @@ public class ApiImplementation
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()))
ApiProcessInput apiProcessInput = apiProcessMetaData.getInput();
if(apiProcessInput != null)
{
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);
processProcessInputFields(paramMap, badRequestMessages, runProcessInput, apiProcessInput.getQueryStringParams());
processProcessInputFields(paramMap, badRequestMessages, runProcessInput, apiProcessInput.getFormParams());
processProcessInputFields(paramMap, badRequestMessages, runProcessInput, apiProcessInput.getObjectBodyParams());
}
// todo! runProcessInput.setRecords(records);
////////////////////////////////////////
// get records for process, if needed //
////////////////////////////////////////
if(process.getMinInputRecords() != null && process.getMinInputRecords() > 0)
{
if(apiProcessInput != null && apiProcessInput.getRecordIdsParamName() != null)
{
String idParam = apiProcessInput.getRecordIdsParamName();
if(StringUtils.hasContent(idParam) && StringUtils.hasContent(paramMap.get(idParam)))
{
String[] ids = paramMap.get(idParam).split(",");
QTableMetaData table = QContext.getQInstance().getTable(process.getTableName());
QQueryFilter filter = new QQueryFilter(new QFilterCriteria(table.getPrimaryKeyField(), IN, Arrays.asList(ids)));
runProcessInput.setCallback(getCallback(filter));
}
}
}
/////////////////////////////////////////
// throw if bad inputs have been noted //
@ -978,8 +995,17 @@ public class ApiImplementation
/////////////////////
// run the process //
/////////////////////
RunProcessAction runProcessAction = new RunProcessAction();
RunProcessOutput runProcessOutput = runProcessAction.execute(runProcessInput);
RunProcessOutput runProcessOutput;
try
{
RunProcessAction runProcessAction = new RunProcessAction();
runProcessOutput = runProcessAction.execute(runProcessInput);
}
catch(CouldNotFindQueryFilterForExtractStepException e)
{
throw (new QBadRequestException("Records to run through this process were not specified."));
}
/////////////////////////////////////////
// run post-customizer, if there is one //
@ -993,12 +1019,42 @@ public class ApiImplementation
///////////////////////
// map output values //
///////////////////////
for(QFieldMetaData outputField : apiProcessMetaData.getOutputFields())
ApiProcessOutputInterface output = apiProcessMetaData.getOutput();
if(output != null)
{
output.put(outputField.getName(), runProcessOutput.getValues().get(outputField.getName()));
return (new HttpApiResponse(output.getSuccessStatusCode(runProcessInput, runProcessOutput), output.getOutputForProcess(runProcessInput, runProcessOutput)));
}
else
{
return (new HttpApiResponse(HttpStatus.Code.NO_CONTENT, ""));
}
}
/*******************************************************************************
**
*******************************************************************************/
private static void processProcessInputFields(Map<String, String> paramMap, List<String> badRequestMessages, RunProcessInput runProcessInput, ApiProcessInputFieldsContainer fieldsContainer)
{
if(fieldsContainer == null)
{
return;
}
return (output);
for(QFieldMetaData inputField : CollectionUtils.nonNullList(fieldsContainer.getFields()))
{
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);
}
}
@ -1233,65 +1289,6 @@ 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);
}
/*******************************************************************************
**
*******************************************************************************/
@ -1333,99 +1330,6 @@ 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);
}
/*******************************************************************************
**
@ -1445,4 +1349,29 @@ public class ApiImplementation
return errors.stream().anyMatch(e -> (e instanceof NotFoundStatusMessage));
}
/*******************************************************************************
**
*******************************************************************************/
private static QProcessCallback getCallback(QQueryFilter filter)
{
return new QProcessCallback()
{
@Override
public QQueryFilter getQueryFilter()
{
return (filter);
}
@Override
public Map<String, Serializable> getFieldValues(List<QFieldMetaData> fields)
{
return (Collections.emptyMap());
}
};
}
}

View File

@ -41,6 +41,11 @@ import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaData;
import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaDataContainer;
import com.kingsrook.qqq.api.model.metadata.ApiOperation;
import com.kingsrook.qqq.api.model.metadata.fields.ApiFieldMetaData;
import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessInput;
import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessInputFieldsContainer;
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.ApiProcessUtils;
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.Components;
@ -73,12 +78,14 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValue;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Association;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Capability;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
import com.kingsrook.qqq.backend.core.utils.ObjectUtils;
import com.kingsrook.qqq.backend.core.utils.Pair;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.YamlUtils;
import com.kingsrook.qqq.backend.core.utils.collections.ListBuilder;
@ -192,8 +199,9 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
*******************************************************************************/
public GenerateOpenApiSpecOutput execute(GenerateOpenApiSpecInput input) throws QException
{
QInstance qInstance = QContext.getQInstance();
String version = input.getVersion();
QInstance qInstance = QContext.getQInstance();
String version = input.getVersion();
APIVersion apiVersion = new APIVersion(version);
ApiInstanceMetaDataContainer apiInstanceMetaDataContainer = ApiInstanceMetaDataContainer.of(qInstance);
if(apiInstanceMetaDataContainer == null)
@ -217,7 +225,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
throw new QException("Missing required input: version");
}
if(!apiInstanceMetaData.getSupportedVersions().contains(new APIVersion(version)))
if(!apiInstanceMetaData.getSupportedVersions().contains(apiVersion))
{
throw (new QException("[" + version + "] is not a supported API Version."));
}
@ -291,7 +299,8 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
///////////////////
// foreach table //
///////////////////
List<QTableMetaData> tables = new ArrayList<>(qInstance.getTables().values());
List<QTableMetaData> tables = new ArrayList<>(qInstance.getTables().values());
Set<String> usedProcessNames = new HashSet<>();
tables.sort(Comparator.comparing(t -> ObjectUtils.requireNonNullElse(t.getLabel(), t.getName(), "")));
for(QTableMetaData table : tables)
{
@ -330,7 +339,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
}
APIVersionRange apiVersionRange = apiTableMetaData.getApiVersionRange();
if(!apiVersionRange.includes(new APIVersion(version)))
if(!apiVersionRange.includes(apiVersion))
{
LOG.debug("Omitting table [" + tableName + "] because its api version range [" + apiVersionRange + "] does not include this version [" + version + "]");
continue;
@ -355,9 +364,11 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
boolean deleteEnabled = ApiOperation.DELETE.isOperationEnabled(operationProviders) && deleteCapability;
boolean deleteBulkEnabled = ApiOperation.BULK_DELETE.isOperationEnabled(operationProviders) && deleteCapability;
if(!getEnabled && !queryByQueryStringEnabled && !insertEnabled && !insertBulkEnabled && !updateEnabled && !updateBulkEnabled && !deleteEnabled && !deleteBulkEnabled)
List<Pair<ApiProcessMetaData, QProcessMetaData>> apiProcessMetaDataList = getProcessesUnderTable(table, apiName, apiVersion);
if(!getEnabled && !queryByQueryStringEnabled && !insertEnabled && !insertBulkEnabled && !updateEnabled && !updateBulkEnabled && !deleteEnabled && !deleteBulkEnabled && !CollectionUtils.nullSafeHasContents(apiProcessMetaDataList))
{
LOG.debug("Omitting table [" + tableName + "] because it does not have any supported capabilities / enabled operations");
LOG.debug("Omitting table [" + tableName + "] because it does not have any supported capabilities / enabled operations or processes");
continue;
}
@ -687,6 +698,35 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
.withPatch(updateBulkEnabled ? bulkPatch : null)
.withDelete(deleteBulkEnabled ? bulkDelete : null));
}
// todo - need to place these differently based on something?
for(Pair<ApiProcessMetaData, QProcessMetaData> pair : CollectionUtils.nonNullList(apiProcessMetaDataList))
{
ApiProcessMetaData apiProcessMetaData = pair.getA();
QProcessMetaData processMetaData = pair.getB();
String processApiPath = ApiProcessUtils.getProcessApiPath(qInstance, processMetaData, apiProcessMetaData, apiInstanceMetaData);
Path path = generateProcessSpecPathObject(apiInstanceMetaData, apiProcessMetaData, processMetaData, ListBuilder.of(tableLabel));
openAPI.getPaths().put(basePath + processApiPath, path);
usedProcessNames.add(processMetaData.getName());
}
}
/////////////////////////////
// add non-table processes //
/////////////////////////////
List<Pair<ApiProcessMetaData, QProcessMetaData>> processesNotUnderTables = getProcessesNotUnderTables(apiName, apiVersion, usedProcessNames);
for(Pair<ApiProcessMetaData, QProcessMetaData> pair : CollectionUtils.nonNullList(processesNotUnderTables))
{
ApiProcessMetaData apiProcessMetaData = pair.getA();
QProcessMetaData processMetaData = pair.getB();
String processApiPath = ApiProcessUtils.getProcessApiPath(qInstance, processMetaData, apiProcessMetaData, apiInstanceMetaData);
Path path = generateProcessSpecPathObject(apiInstanceMetaData, apiProcessMetaData, processMetaData, ListBuilder.of(processMetaData.getLabel()));
openAPI.getPaths().put(basePath + processApiPath, path);
usedProcessNames.add(processMetaData.getName());
}
componentResponses.put("error" + HttpStatus.BAD_REQUEST.getCode(), buildStandardErrorResponse("Bad Request. Some portion of the request's content was not acceptable to the server. See error message in body for details.", "Parameter id should be given an integer value, but received string: \"Foo\""));
@ -710,6 +750,140 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
/*******************************************************************************
**
*******************************************************************************/
private List<Pair<ApiProcessMetaData, QProcessMetaData>> getProcessesNotUnderTables(String apiName, APIVersion apiVersion, Set<String> usedProcessNames)
{
List<Pair<ApiProcessMetaData, QProcessMetaData>> apiProcessMetaDataList = new ArrayList<>();
for(QProcessMetaData processMetaData : CollectionUtils.nonNullMap(QContext.getQInstance().getProcesses()).values())
{
if(usedProcessNames.contains(processMetaData.getName()))
{
continue;
}
ApiProcessMetaDataContainer apiProcessMetaDataContainer = ApiProcessMetaDataContainer.of(processMetaData);
if(apiProcessMetaDataContainer == null)
{
continue;
}
ApiProcessMetaData apiProcessMetaData = apiProcessMetaDataContainer.getApis().get(apiName);
if(apiProcessMetaData == null)
{
continue;
}
if(!apiProcessMetaData.getApiVersionRange().includes(apiVersion))
{
continue;
}
apiProcessMetaDataList.add(Pair.of(apiProcessMetaData, processMetaData));
}
return (apiProcessMetaDataList);
}
/*******************************************************************************
**
*******************************************************************************/
private Path generateProcessSpecPathObject(ApiInstanceMetaData apiInstanceMetaData, ApiProcessMetaData apiProcessMetaData, QProcessMetaData processMetaData, List<String> tags)
{
Method methodForProcess = new Method()
.withOperationId(apiProcessMetaData.getApiProcessName())
.withTags(tags)
.withSummary(processMetaData.getLabel()) // todo - add optional summary to meta data
.withDescription("Run the process named " + processMetaData.getLabel())// todo - add optional description to meta data, .withDescription()
.withSecurity(getSecurity(apiInstanceMetaData, "todo - process name"));
List<Parameter> parameters = new ArrayList<>();
ApiProcessInput apiProcessInput = apiProcessMetaData.getInput();
if(apiProcessInput != null)
{
ApiProcessInputFieldsContainer queryStringParams = apiProcessInput.getQueryStringParams();
if(queryStringParams != null)
{
for(QFieldMetaData field : CollectionUtils.nonNullList(queryStringParams.getFields()))
{
parameters.add(new Parameter()
.withName(field.getName())
// todo - add description to meta data .withDescription("Which page of results to return. Starts at 1.")
.withDescription("Value for the " + field.getLabel() + " field.")
.withIn("query")
.withRequired(field.getIsRequired())
.withSchema(new Schema().withType(getFieldType(field))));
}
}
}
if(CollectionUtils.nullSafeHasContents(parameters))
{
methodForProcess.setParameters(parameters);
}
// todo methodForProcess.withRequestBody();
// todo methodForProcess.withResponse();
methodForProcess.withResponse(HttpStatus.OK.getCode(), new Response()
.withDescription("Successfully ran the process")
.withContent(MapBuilder.of("application/json", new Content())));
@SuppressWarnings("checkstyle:indentation")
Path path = switch(apiProcessMetaData.getMethod())
{
case GET -> new Path().withGet(methodForProcess);
case POST -> new Path().withPost(methodForProcess);
case PUT -> new Path().withPut(methodForProcess);
case PATCH -> new Path().withPatch(methodForProcess);
case DELETE -> new Path().withDelete(methodForProcess);
};
return (path);
}
/*******************************************************************************
**
*******************************************************************************/
private List<Pair<ApiProcessMetaData, QProcessMetaData>> getProcessesUnderTable(QTableMetaData table, String apiName, APIVersion apiVersion)
{
List<Pair<ApiProcessMetaData, QProcessMetaData>> apiProcessMetaDataList = new ArrayList<>();
for(QProcessMetaData processMetaData : CollectionUtils.nonNullMap(QContext.getQInstance().getProcesses()).values())
{
if(!table.getName().equals(processMetaData.getTableName()))
{
continue;
}
ApiProcessMetaDataContainer apiProcessMetaDataContainer = ApiProcessMetaDataContainer.of(processMetaData);
if(apiProcessMetaDataContainer == null)
{
continue;
}
ApiProcessMetaData apiProcessMetaData = apiProcessMetaDataContainer.getApis().get(apiName);
if(apiProcessMetaData == null)
{
continue;
}
if(!apiProcessMetaData.getApiVersionRange().includes(apiVersion))
{
continue;
}
apiProcessMetaDataList.add(Pair.of(apiProcessMetaData, processMetaData));
}
return (apiProcessMetaDataList);
}
/*******************************************************************************
** written for the use-case of, generating a single table's api, but it has
** associations that it references, so we need their schemas too - so, make

View File

@ -28,11 +28,13 @@ import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.BiFunction;
import java.util.stream.Collectors;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.kingsrook.qqq.api.actions.ApiImplementation;
@ -41,10 +43,15 @@ import com.kingsrook.qqq.api.model.APILog;
import com.kingsrook.qqq.api.model.APIVersion;
import com.kingsrook.qqq.api.model.actions.GenerateOpenApiSpecInput;
import com.kingsrook.qqq.api.model.actions.GenerateOpenApiSpecOutput;
import com.kingsrook.qqq.api.model.actions.HttpApiResponse;
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.ApiProcessInput;
import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessInputFieldsContainer;
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.ApiProcessUtils;
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;
@ -80,6 +87,7 @@ import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModu
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.ExceptionUtils;
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
import com.kingsrook.qqq.backend.core.utils.ObjectUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder;
import com.kingsrook.qqq.backend.javalin.QJavalinAccessLogger;
@ -91,6 +99,7 @@ import io.javalin.http.Context;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.BooleanUtils;
import org.eclipse.jetty.http.HttpStatus;
import org.json.JSONObject;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
import static com.kingsrook.qqq.backend.javalin.QJavalinImplementation.SLOW_LOG_THRESHOLD_MS;
@ -102,6 +111,8 @@ public class QJavalinApiHandler
{
private static final QLogger LOG = QLogger.getLogger(QJavalinApiHandler.class);
private static final ApiProcessMetaDataContainer EMPTY_CONTAINER = new ApiProcessMetaDataContainer().withApis(Collections.emptyMap());
private static QInstance qInstance;
private static Map<String, Integer> apiLogUserIdCache = new HashMap<>();
@ -174,10 +185,11 @@ public class QJavalinApiHandler
///////////////////
for(QProcessMetaData process : qInstance.getProcesses().values())
{
ApiProcessMetaData apiProcessMetaData = ApiImplementation.getApiProcessMetaDataIfProcessIsInApi(apiInstanceMetaData, process);
ApiProcessMetaDataContainer apiProcessMetaDataContainer = Objects.requireNonNullElse(ApiProcessMetaDataContainer.of(process), EMPTY_CONTAINER);
ApiProcessMetaData apiProcessMetaData = apiProcessMetaDataContainer.getApis().get(apiInstanceMetaData.getName());
if(apiProcessMetaData != null)
{
String path = getProcessApiPath(process, apiProcessMetaData, apiInstanceMetaData);
String path = ApiProcessUtils.getProcessApiPath(qInstance, process, apiProcessMetaData, apiInstanceMetaData);
HttpMethod method = apiProcessMetaData.getMethod();
switch(method)
{
@ -189,6 +201,8 @@ public class QJavalinApiHandler
default -> throw (new QRuntimeException("Unrecognized http method [" + method + "] for process [" + process.getName() + "]"));
}
make405sForOtherMethods(method, path);
if(doesProcessSupportAsync(apiInstanceMetaData, process))
{
ApiBuilder.get(path + "/status/{processId}", context -> getProcessStatus(context, apiInstanceMetaData));
@ -247,6 +261,49 @@ public class QJavalinApiHandler
/*******************************************************************************
**
*******************************************************************************/
private void make405sForOtherMethods(HttpMethod allowedMethod, String path)
{
if(!allowedMethod.equals(HttpMethod.GET))
{
ApiBuilder.get(path, (Context c) -> QJavalinApiHandler.return405(c, allowedMethod));
}
if(!allowedMethod.equals(HttpMethod.POST))
{
ApiBuilder.post(path, (Context c) -> QJavalinApiHandler.return405(c, allowedMethod));
}
if(!allowedMethod.equals(HttpMethod.PUT))
{
ApiBuilder.put(path, (Context c) -> QJavalinApiHandler.return405(c, allowedMethod));
}
if(!allowedMethod.equals(HttpMethod.PATCH))
{
ApiBuilder.patch(path, (Context c) -> QJavalinApiHandler.return405(c, allowedMethod));
}
if(!allowedMethod.equals(HttpMethod.DELETE))
{
ApiBuilder.delete(path, (Context c) -> QJavalinApiHandler.return405(c, allowedMethod));
}
}
/*******************************************************************************
**
*******************************************************************************/
private static void return405(Context context, HttpMethod allowedMethod)
{
respondWithError(context, HttpStatus.Code.METHOD_NOT_ALLOWED, "This path only supports method: " + allowedMethod, newAPILog(context)); // 405
}
/*******************************************************************************
**
*******************************************************************************/
@ -272,21 +329,26 @@ public class QJavalinApiHandler
QJavalinAccessLogger.logStart("apiRunProcess", logPair("process", processMetaData.getName()));
Map<String, String> parameters = new LinkedHashMap<>();
for(QFieldMetaData inputField : CollectionUtils.nonNullList(apiProcessMetaData.getInputFields()))
ApiProcessInput input = apiProcessMetaData.getInput();
if(input != null)
{
String value = switch(apiProcessMetaData.getMethod())
processProcessInputFieldsContainer(context, parameters, input.getQueryStringParams(), Context::queryParam);
processProcessInputFieldsContainer(context, parameters, input.getFormParams(), Context::formParam);
ApiProcessInputFieldsContainer objectBodyParams = input.getObjectBodyParams();
if(objectBodyParams != null)
{
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);
JSONObject jsonObject = new JSONObject(context.body());
processProcessInputFieldsContainer(context, parameters, objectBodyParams, (ctx, name) -> jsonObject.optString(name, null));
}
}
Map<String, Serializable> outputRecord = ApiImplementation.runProcess(apiInstanceMetaData, version, apiProcessMetaData.getApiProcessName(), parameters);
HttpApiResponse response = ApiImplementation.runProcess(apiInstanceMetaData, version, apiProcessMetaData.getApiProcessName(), parameters);
context.status(response.getStatusCode().getCode());
QJavalinAccessLogger.logEndSuccess();
String resultString = toJson(outputRecord);
String resultString = toJson(Objects.requireNonNullElse(response.getResponseBodyObject(), ""));
context.result(resultString);
storeApiLog(apiLog.withStatusCode(context.statusCode()).withResponseBody(resultString));
}
@ -302,10 +364,22 @@ public class QJavalinApiHandler
/*******************************************************************************
**
*******************************************************************************/
private boolean doesProcessSupportAsync(ApiInstanceMetaData apiInstanceMetaData, QProcessMetaData process)
private static void processProcessInputFieldsContainer(Context context, Map<String, String> parameters, ApiProcessInputFieldsContainer fieldsContainer, BiFunction<Context, String, String> paramAccessor)
{
// todo - implement
return false;
if(fieldsContainer != null)
{
List<QFieldMetaData> fields = CollectionUtils.nonNullList(fieldsContainer.getFields());
ObjectUtils.ifNotNull(fieldsContainer.getRecordIdsField(), fields::add);
for(QFieldMetaData field : fields)
{
String queryParamValue = paramAccessor.apply(context, field.getName());
if(queryParamValue != null)
{
String backendName = ObjectUtils.requireConditionElse(field.getBackendName(), StringUtils::hasContent, field.getName());
parameters.put(backendName, queryParamValue);
}
}
}
}
@ -313,34 +387,10 @@ public class QJavalinApiHandler
/*******************************************************************************
**
*******************************************************************************/
private String getProcessApiPath(QProcessMetaData process, ApiProcessMetaData apiProcessMetaData, ApiInstanceMetaData apiInstanceMetaData)
private boolean doesProcessSupportAsync(ApiInstanceMetaData apiInstanceMetaData, QProcessMetaData process)
{
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();
}
// todo - implement
return false;
}

View File

@ -0,0 +1,122 @@
/*
* 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.actions;
import java.io.Serializable;
import org.eclipse.jetty.http.HttpStatus;
/*******************************************************************************
** class to contain http api responses.
**
*******************************************************************************/
public class HttpApiResponse
{
private HttpStatus.Code statusCode;
private Serializable responseBodyObject;
/*******************************************************************************
** Default Constructor
**
*******************************************************************************/
public HttpApiResponse()
{
}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public HttpApiResponse(HttpStatus.Code statusCode, Serializable responseBodyObject)
{
this.statusCode = statusCode;
this.responseBodyObject = responseBodyObject;
}
/*******************************************************************************
** Getter for statusCode
*******************************************************************************/
public HttpStatus.Code getStatusCode()
{
return (this.statusCode);
}
/*******************************************************************************
** Setter for statusCode
*******************************************************************************/
public void setStatusCode(HttpStatus.Code statusCode)
{
this.statusCode = statusCode;
}
/*******************************************************************************
** Fluent setter for statusCode
*******************************************************************************/
public HttpApiResponse withStatusCode(HttpStatus.Code statusCode)
{
this.statusCode = statusCode;
return (this);
}
/*******************************************************************************
** Getter for responseBodyObject
*******************************************************************************/
public Serializable getResponseBodyObject()
{
return (this.responseBodyObject);
}
/*******************************************************************************
** Setter for responseBodyObject
*******************************************************************************/
public void setResponseBodyObject(Serializable responseBodyObject)
{
this.responseBodyObject = responseBodyObject;
}
/*******************************************************************************
** Fluent setter for responseBodyObject
*******************************************************************************/
public HttpApiResponse withResponseBodyObject(Serializable responseBodyObject)
{
this.responseBodyObject = responseBodyObject;
return (this);
}
}

View File

@ -0,0 +1,130 @@
package com.kingsrook.qqq.api.model.metadata.processes;
/*******************************************************************************
**
*******************************************************************************/
public class ApiProcessInput
{
private ApiProcessInputFieldsContainer queryStringParams;
private ApiProcessInputFieldsContainer formParams;
private ApiProcessInputFieldsContainer recordBodyParams;
/*******************************************************************************
**
*******************************************************************************/
public String getRecordIdsParamName()
{
if(queryStringParams != null && queryStringParams.getRecordIdsField() != null)
{
return (queryStringParams.getRecordIdsField().getName());
}
if(formParams != null && formParams.getRecordIdsField() != null)
{
return (formParams.getRecordIdsField().getName());
}
if(recordBodyParams != null && recordBodyParams.getRecordIdsField() != null)
{
return (recordBodyParams.getRecordIdsField().getName());
}
return (null);
}
/*******************************************************************************
** Getter for queryStringParams
*******************************************************************************/
public ApiProcessInputFieldsContainer getQueryStringParams()
{
return (this.queryStringParams);
}
/*******************************************************************************
** Setter for queryStringParams
*******************************************************************************/
public void setQueryStringParams(ApiProcessInputFieldsContainer queryStringParams)
{
this.queryStringParams = queryStringParams;
}
/*******************************************************************************
** Fluent setter for queryStringParams
*******************************************************************************/
public ApiProcessInput withQueryStringParams(ApiProcessInputFieldsContainer queryStringParams)
{
this.queryStringParams = queryStringParams;
return (this);
}
/*******************************************************************************
** Getter for formParams
*******************************************************************************/
public ApiProcessInputFieldsContainer getFormParams()
{
return (this.formParams);
}
/*******************************************************************************
** Setter for formParams
*******************************************************************************/
public void setFormParams(ApiProcessInputFieldsContainer formParams)
{
this.formParams = formParams;
}
/*******************************************************************************
** Fluent setter for formParams
*******************************************************************************/
public ApiProcessInput withFormParams(ApiProcessInputFieldsContainer formParams)
{
this.formParams = formParams;
return (this);
}
/*******************************************************************************
** Getter for recordBodyParams
*******************************************************************************/
public ApiProcessInputFieldsContainer getObjectBodyParams()
{
return (this.recordBodyParams);
}
/*******************************************************************************
** Setter for recordBodyParams
*******************************************************************************/
public void setRecordBodyParams(ApiProcessInputFieldsContainer recordBodyParams)
{
this.recordBodyParams = recordBodyParams;
}
/*******************************************************************************
** Fluent setter for recordBodyParams
*******************************************************************************/
public ApiProcessInput withRecordBodyParams(ApiProcessInputFieldsContainer recordBodyParams)
{
this.recordBodyParams = recordBodyParams;
return (this);
}
}

View File

@ -0,0 +1,116 @@
package com.kingsrook.qqq.api.model.metadata.processes;
import java.util.ArrayList;
import java.util.List;
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;
/*******************************************************************************
**
*******************************************************************************/
public class ApiProcessInputFieldsContainer
{
private QFieldMetaData recordIdsField;
private List<QFieldMetaData> fields;
/*******************************************************************************
**
*******************************************************************************/
public ApiProcessInputFieldsContainer withInferredInputFields(QProcessMetaData processMetaData)
{
fields = new ArrayList<>();
for(QStepMetaData stepMetaData : CollectionUtils.nonNullList(processMetaData.getStepList()))
{
if(stepMetaData instanceof QFrontendStepMetaData frontendStep)
{
fields.addAll(frontendStep.getInputFields());
}
}
return (this);
}
/*******************************************************************************
** Getter for recordIdsField
*******************************************************************************/
public QFieldMetaData getRecordIdsField()
{
return (this.recordIdsField);
}
/*******************************************************************************
** Setter for recordIdsField
*******************************************************************************/
public void setRecordIdsField(QFieldMetaData recordIdsField)
{
this.recordIdsField = recordIdsField;
}
/*******************************************************************************
** Fluent setter for recordIdsField
*******************************************************************************/
public ApiProcessInputFieldsContainer withRecordIdsField(QFieldMetaData recordIdsField)
{
this.recordIdsField = recordIdsField;
return (this);
}
/*******************************************************************************
** Getter for fields
*******************************************************************************/
public List<QFieldMetaData> getFields()
{
return (this.fields);
}
/*******************************************************************************
** Setter for fields
*******************************************************************************/
public void setFields(List<QFieldMetaData> fields)
{
this.fields = fields;
}
/*******************************************************************************
** Fluent setter for fields
*******************************************************************************/
public ApiProcessInputFieldsContainer withField(QFieldMetaData field)
{
if(this.fields == null)
{
this.fields = new ArrayList<>();
}
this.fields.add(field);
return (this);
}
/*******************************************************************************
** Fluent setter for fields
*******************************************************************************/
public ApiProcessInputFieldsContainer withFields(List<QFieldMetaData> fields)
{
this.fields = fields;
return (this);
}
}

View File

@ -22,7 +22,6 @@
package com.kingsrook.qqq.api.model.metadata.processes;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -31,13 +30,13 @@ 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.instances.QInstanceEnricher;
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;
import com.kingsrook.qqq.backend.core.utils.collections.ListBuilder;
/*******************************************************************************
@ -54,51 +53,13 @@ public class ApiProcessMetaData
private String path;
private HttpMethod method;
private List<QFieldMetaData> inputFields;
private List<QFieldMetaData> outputFields;
private ApiProcessInput input;
private ApiProcessOutputInterface output;
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);
}
/*******************************************************************************
**
*******************************************************************************/
@ -119,8 +80,7 @@ public class ApiProcessMetaData
/*******************************************************************************
**
*******************************************************************************/
@SuppressWarnings("unchecked")
public void enrich(String apiName, QProcessMetaData process)
public void enrich(QInstanceEnricher qInstanceEnricher, String apiName, QProcessMetaData process)
{
if(!StringUtils.hasContent(getApiProcessName()))
{
@ -129,15 +89,19 @@ public class ApiProcessMetaData
if(initialVersion != null)
{
///////////////////////////////////////////////////////////////
// make sure all fields have at least an initial version set //
///////////////////////////////////////////////////////////////
for(QFieldMetaData field : CollectionUtils.mergeLists(getInputFields(), getOutputFields()))
if(getOutput() instanceof ApiProcessObjectOutput outputObject)
{
ApiFieldMetaData apiFieldMetaData = ensureFieldHasApiSupplementalMetaData(apiName, field);
if(apiFieldMetaData.getInitialVersion() == null)
enrichFieldList(qInstanceEnricher, apiName, outputObject.getOutputFields());
}
if(input != null)
{
for(ApiProcessInputFieldsContainer fieldsContainer : ListBuilder.of(input.getQueryStringParams(), input.getFormParams(), input.getObjectBodyParams()))
{
apiFieldMetaData.setInitialVersion(initialVersion);
if(fieldsContainer != null)
{
enrichFieldList(qInstanceEnricher, apiName, fieldsContainer.getFields());
}
}
}
}
@ -145,6 +109,25 @@ public class ApiProcessMetaData
/*******************************************************************************
**
*******************************************************************************/
private void enrichFieldList(QInstanceEnricher qInstanceEnricher, String apiName, List<QFieldMetaData> fields)
{
for(QFieldMetaData field : CollectionUtils.nonNullList(fields))
{
ApiFieldMetaData apiFieldMetaData = ensureFieldHasApiSupplementalMetaData(apiName, field);
if(apiFieldMetaData.getInitialVersion() == null)
{
apiFieldMetaData.setInitialVersion(initialVersion);
}
qInstanceEnricher.enrichField(field);
}
}
/*******************************************************************************
**
*******************************************************************************/
@ -166,36 +149,6 @@ public class ApiProcessMetaData
/*******************************************************************************
** 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
*******************************************************************************/
@ -382,68 +335,6 @@ public class ApiProcessMetaData
/*******************************************************************************
** 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
*******************************************************************************/
@ -493,4 +384,66 @@ public class ApiProcessMetaData
return (this);
}
/*******************************************************************************
** Getter for output
*******************************************************************************/
public ApiProcessOutputInterface getOutput()
{
return (this.output);
}
/*******************************************************************************
** Setter for output
*******************************************************************************/
public void setOutput(ApiProcessOutputInterface output)
{
this.output = output;
}
/*******************************************************************************
** Fluent setter for output
*******************************************************************************/
public ApiProcessMetaData withOutput(ApiProcessOutputInterface output)
{
this.output = output;
return (this);
}
/*******************************************************************************
** Getter for input
*******************************************************************************/
public ApiProcessInput getInput()
{
return (this.input);
}
/*******************************************************************************
** Setter for input
*******************************************************************************/
public void setInput(ApiProcessInput input)
{
this.input = input;
}
/*******************************************************************************
** Fluent setter for input
*******************************************************************************/
public ApiProcessMetaData withInput(ApiProcessInput input)
{
this.input = input;
return (this);
}
}

View File

@ -25,6 +25,7 @@ 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.instances.QInstanceEnricher;
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;
@ -64,13 +65,13 @@ public class ApiProcessMetaDataContainer extends QSupplementalProcessMetaData
**
*******************************************************************************/
@Override
public void enrich(QProcessMetaData process)
public void enrich(QInstanceEnricher qInstanceEnricher, QProcessMetaData process)
{
super.enrich(process);
super.enrich(qInstanceEnricher, process);
for(Map.Entry<String, ApiProcessMetaData> entry : CollectionUtils.nonNullMap(apis).entrySet())
{
entry.getValue().enrich(entry.getKey(), process);
entry.getValue().enrich(qInstanceEnricher, entry.getKey(), process);
}
}

View File

@ -0,0 +1,85 @@
package com.kingsrook.qqq.api.model.metadata.processes;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
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.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
/*******************************************************************************
**
*******************************************************************************/
public class ApiProcessObjectOutput implements ApiProcessOutputInterface
{
private List<QFieldMetaData> outputFields;
/*******************************************************************************
**
******************************************************************************/
@Override
public Serializable getOutputForProcess(RunProcessInput runProcessInput, RunProcessOutput runProcessOutput)
{
LinkedHashMap<String, Serializable> outputMap = new LinkedHashMap<>();
for(QFieldMetaData outputField : CollectionUtils.nonNullList(getOutputFields()))
{
outputMap.put(outputField.getName(), runProcessOutput.getValues().get(outputField.getName()));
}
return (outputMap);
}
/*******************************************************************************
** 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 ApiProcessObjectOutput withOutputFields(List<QFieldMetaData> outputFields)
{
this.outputFields = outputFields;
return (this);
}
/*******************************************************************************
** Fluent setter for a single outputField
*******************************************************************************/
public ApiProcessObjectOutput withOutputField(QFieldMetaData outputField)
{
if(this.outputFields == null)
{
this.outputFields = new ArrayList<>();
}
this.outputFields.add(outputField);
return (this);
}
}

View File

@ -0,0 +1,30 @@
package com.kingsrook.qqq.api.model.metadata.processes;
import java.io.Serializable;
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;
import org.eclipse.jetty.http.HttpStatus;
/*******************************************************************************
**
*******************************************************************************/
public interface ApiProcessOutputInterface
{
/*******************************************************************************
**
*******************************************************************************/
Serializable getOutputForProcess(RunProcessInput runProcessInput, RunProcessOutput runProcessOutput) throws QException;
/*******************************************************************************
**
*******************************************************************************/
default HttpStatus.Code getSuccessStatusCode(RunProcessInput runProcessInput, RunProcessOutput runProcessOutput)
{
return (HttpStatus.Code.OK);
}
}

View File

@ -0,0 +1,138 @@
package com.kingsrook.qqq.api.model.metadata.processes;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryFilterLink;
import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLine;
import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLineInterface;
import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryRecordLink;
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.utils.CollectionUtils;
import org.apache.commons.lang.NotImplementedException;
import org.eclipse.jetty.http.HttpStatus;
/*******************************************************************************
**
*******************************************************************************/
public class ApiProcessSummaryListOutput implements ApiProcessOutputInterface
{
private static final QLogger LOG = QLogger.getLogger(ApiProcessSummaryListOutput.class);
/*******************************************************************************
**
*******************************************************************************/
@Override
public HttpStatus.Code getSuccessStatusCode(RunProcessInput runProcessInput, RunProcessOutput runProcessOutput)
{
List<ProcessSummaryLineInterface> processSummaryLineInterfaces = (List<ProcessSummaryLineInterface>) runProcessOutput.getValues().get("processResults");
if(processSummaryLineInterfaces.isEmpty())
{
//////////////////////////////////////////////////////////////////////////
// if there are no summary lines, all we can return is 204 - no content //
//////////////////////////////////////////////////////////////////////////
return (HttpStatus.Code.NO_CONTENT);
}
else
{
///////////////////////////////////////////////////////////////////////////////////
// else if there are summary lines, we'll represent them as a 207 - multi-status //
///////////////////////////////////////////////////////////////////////////////////
return (HttpStatus.Code.MULTI_STATUS);
}
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public Serializable getOutputForProcess(RunProcessInput runProcessInput, RunProcessOutput runProcessOutput) throws QException
{
try
{
ArrayList<Serializable> apiOutput = new ArrayList<>();
List<ProcessSummaryLineInterface> processSummaryLineInterfaces = (List<ProcessSummaryLineInterface>) runProcessOutput.getValues().get("processResults");
for(ProcessSummaryLineInterface processSummaryLineInterface : processSummaryLineInterfaces)
{
if(processSummaryLineInterface instanceof ProcessSummaryLine processSummaryLine)
{
processSummaryLine.setCount(1);
processSummaryLine.prepareForFrontend(true);
List<Serializable> primaryKeys = processSummaryLine.getPrimaryKeys();
if(CollectionUtils.nullSafeHasContents(primaryKeys))
{
for(Serializable primaryKey : primaryKeys)
{
HashMap<String, Serializable> map = toMap(processSummaryLine);
map.put("id", primaryKey);
apiOutput.add(map);
}
}
else
{
apiOutput.add(toMap(processSummaryLine));
}
}
else if(processSummaryLineInterface instanceof ProcessSummaryRecordLink processSummaryRecordLink)
{
throw new NotImplementedException("ProcessSummaryRecordLink handling");
}
else if(processSummaryLineInterface instanceof ProcessSummaryFilterLink processSummaryFilterLink)
{
throw new NotImplementedException("ProcessSummaryFilterLink handling");
}
else
{
throw new NotImplementedException("Unknown ProcessSummaryLineInterface handling");
}
}
return (apiOutput);
}
catch(Exception e)
{
LOG.warn("Error getting api output for process", e);
throw (new QException("Error generating process output", e));
}
}
/*******************************************************************************
**
*******************************************************************************/
@SuppressWarnings("checkstyle:indentation")
private static HashMap<String, Serializable> toMap(ProcessSummaryLine processSummaryLine)
{
HashMap<String, Serializable> map = new HashMap<>();
HttpStatus.Code code = switch(processSummaryLine.getStatus())
{
case OK, WARNING, INFO -> HttpStatus.Code.OK;
case ERROR -> HttpStatus.Code.INTERNAL_SERVER_ERROR;
};
String messagePrefix = switch(processSummaryLine.getStatus())
{
case OK, INFO, ERROR -> "";
case WARNING -> "Warning: ";
};
map.put("statusCode", code.getCode());
map.put("statusText", code.getMessage());
map.put("message", messagePrefix + processSummaryLine.getMessage());
return (map);
}
}

View File

@ -0,0 +1,169 @@
package com.kingsrook.qqq.api.model.metadata.processes;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.api.model.APIVersion;
import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaData;
import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaData;
import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaDataContainer;
import com.kingsrook.qqq.backend.core.context.QContext;
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.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.Pair;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import org.apache.commons.lang.BooleanUtils;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
**
*******************************************************************************/
public class ApiProcessUtils
{
private static final QLogger LOG = QLogger.getLogger(ApiProcessUtils.class);
private static Map<Pair<String, String>, Map<String, QProcessMetaData>> processApiNameMap = new HashMap<>();
/*******************************************************************************
**
*******************************************************************************/
public static Pair<ApiProcessMetaData, QProcessMetaData> getProcessMetaDataPair(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 (Pair.of(apiProcessMetaData, process));
}
/*******************************************************************************
**
*******************************************************************************/
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 String getProcessApiPath(QInstance qInstance, 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

@ -29,8 +29,12 @@ 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.ApiProcessInput;
import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessInputFieldsContainer;
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.ApiProcessObjectOutput;
import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessSummaryListOutput;
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;
@ -72,8 +76,10 @@ import com.kingsrook.qqq.backend.core.model.statusmessages.BadInputStatusMessage
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.processes.implementations.etl.streamedwithfrontend.ExtractViaQueryStep;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.LoadViaUpdateStep;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.StreamedETLWithFrontendProcess;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.collections.ListBuilder;
/*******************************************************************************
@ -89,7 +95,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 PROCESS_NAME_GET_PERSON_INFO = "getPersonInfo";
public static final String PROCESS_NAME_TRANSFORM_PEOPLE = "transformPeople";
public static final String API_NAME = "test-api";
public static final String ALTERNATIVE_API_NAME = "person-api";
@ -122,6 +129,7 @@ public class TestUtils
qInstance.addPossibleValueSource(definePersonPossibleValueSource());
qInstance.addProcess(defineProcessGetPersonInfo());
qInstance.addProcess(defineProcessTransformPeople());
qInstance.setAuthentication(new Auth0AuthenticationMetaData().withType(QAuthenticationType.FULLY_ANONYMOUS).withName("anonymous"));
@ -214,12 +222,12 @@ public class TestUtils
.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)
))
.withInput(new ApiProcessInput()
.withQueryStringParams(new ApiProcessInputFieldsContainer().withInferredInputFields(process)))
.withOutput(new ApiProcessObjectOutput()
.withOutputField(new QFieldMetaData("density", QFieldType.DECIMAL))
.withOutputField(new QFieldMetaData("daysOld", QFieldType.INTEGER))
.withOutputField(new QFieldMetaData("nickname", QFieldType.STRING)))
));
return (process);
@ -227,6 +235,35 @@ public class TestUtils
/*******************************************************************************
**
*******************************************************************************/
private static QProcessMetaData defineProcessTransformPeople()
{
QProcessMetaData process = StreamedETLWithFrontendProcess.processMetaDataBuilder()
.withName(PROCESS_NAME_TRANSFORM_PEOPLE)
.withTableName(TABLE_NAME_PERSON)
.withSourceTable(TABLE_NAME_PERSON)
.withDestinationTable(TABLE_NAME_PERSON)
.withMinInputRecords(1)
.withExtractStepClass(ExtractViaQueryStep.class)
.withTransformStepClass(TransformPersonStep.class)
.withLoadStepClass(LoadViaUpdateStep.class)
.getProcessMetaData();
process.withSupplementalMetaData(new ApiProcessMetaDataContainer()
.withApiProcessMetaData(API_NAME, new ApiProcessMetaData()
.withInitialVersion(CURRENT_API_VERSION)
.withMethod(HttpMethod.POST)
.withInput(new ApiProcessInput()
.withQueryStringParams(new ApiProcessInputFieldsContainer().withRecordIdsField(new QFieldMetaData("id", QFieldType.STRING))))
.withOutput(new ApiProcessSummaryListOutput())));
return (process);
}
/*******************************************************************************
** Define the in-memory backend used in standard tests
*******************************************************************************/

View File

@ -0,0 +1,59 @@
package com.kingsrook.qqq.api;
import java.util.ArrayList;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLine;
import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLineInterface;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.AbstractTransformStep;
import com.kingsrook.qqq.backend.core.processes.implementations.general.StandardProcessSummaryLineProducer;
/*******************************************************************************
**
*******************************************************************************/
public class TransformPersonStep extends AbstractTransformStep
{
private ProcessSummaryLine okLine = StandardProcessSummaryLineProducer.getOkToUpdateLine();
private ProcessSummaryLine errorLine = StandardProcessSummaryLineProducer.getErrorLine();
/*******************************************************************************
**
*******************************************************************************/
@Override
public ArrayList<ProcessSummaryLineInterface> getProcessSummary(RunBackendStepOutput runBackendStepOutput, boolean isForResultScreen)
{
ArrayList<ProcessSummaryLineInterface> rs = new ArrayList<>();
okLine.addSelfToListIfAnyCount(rs);
errorLine.addSelfToListIfAnyCount(rs);
return (rs);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
{
for(QRecord record : runBackendStepInput.getRecords())
{
Integer id = record.getValueInteger("id");
if(id % 2 == 0)
{
okLine.incrementCountAndAddPrimaryKey(id);
}
else
{
errorLine.incrementCountAndAddPrimaryKey(id);
}
}
}
}

View File

@ -1443,9 +1443,12 @@ class QJavalinApiHandlerTest extends BaseTest
**
*******************************************************************************/
@Test
void testProcess() throws QException
void testGetProcessForObject() throws QException
{
HttpResponse<String> response = Unirest.get(BASE_URL + "/api/" + VERSION + "/person/getPersonInfo?age=43&partnerPersonId=1&heightInches=72&weightPounds=220&homeTown=Chester").asString();
HttpResponse<String> response = Unirest.post(BASE_URL + "/api/" + VERSION + "/person/getPersonInfo").asString();
assertErrorResponse(HttpStatus.METHOD_NOT_ALLOWED_405, "This path only supports method: GET", response);
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));
@ -1453,6 +1456,33 @@ class QJavalinApiHandlerTest extends BaseTest
/*******************************************************************************
**
*******************************************************************************/
@Test
void testPostProcessForProcessSummaryList() throws QException
{
insertSimpsons();
HttpResponse<String> response = Unirest.get(BASE_URL + "/api/" + VERSION + "/person/transformPeople").asString();
assertErrorResponse(HttpStatus.METHOD_NOT_ALLOWED_405, "This path only supports method: POST", response);
response = Unirest.post(BASE_URL + "/api/" + VERSION + "/person/transformPeople").asString();
assertErrorResponse(HttpStatus.BAD_REQUEST_400, "Records to run through this process were not specified", response);
response = Unirest.post(BASE_URL + "/api/" + VERSION + "/person/transformPeople?id=999").asString();
assertEquals(HttpStatus.NO_CONTENT_204, response.getStatus());
assertEquals("", response.getBody());
response = Unirest.post(BASE_URL + "/api/" + VERSION + "/person/transformPeople?id=1,2,3").asString();
assertEquals(HttpStatus.MULTI_STATUS_207, response.getStatus());
JSONArray jsonArray = new JSONArray(response.getBody());
assertEquals(3, jsonArray.length());
System.out.println(jsonArray.toString(3));
}
/*******************************************************************************
**
*******************************************************************************/