Merge branch 'integration/sprint-27' into feature/CTLE-509-support-sending-custom-data-fields-to-deposco

This commit is contained in:
2023-06-20 10:37:20 -05:00
committed by GitHub
61 changed files with 4854 additions and 195 deletions

View File

@ -25,7 +25,7 @@ package com.kingsrook.qqq.api;
/*******************************************************************************
**
*******************************************************************************/
public interface ApiMiddlewareType
public interface ApiSupplementType
{
String NAME = "api";

View File

@ -25,19 +25,39 @@ 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;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import com.kingsrook.qqq.api.javalin.QBadRequestException;
import com.kingsrook.qqq.api.model.APIVersion;
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.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;
import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaDataContainer;
import com.kingsrook.qqq.backend.core.actions.async.AsyncJobManager;
import com.kingsrook.qqq.backend.core.actions.async.AsyncJobState;
import com.kingsrook.qqq.backend.core.actions.async.AsyncJobStatus;
import com.kingsrook.qqq.backend.core.actions.async.JobGoingAsyncException;
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;
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
@ -49,6 +69,9 @@ import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QNotFoundException;
import com.kingsrook.qqq.backend.core.logging.LogPair;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessState;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.QInputSource;
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput;
@ -67,8 +90,10 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.statusmessages.BadInputStatusMessage;
import com.kingsrook.qqq.backend.core.model.statusmessages.NotFoundStatusMessage;
@ -76,7 +101,9 @@ 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.ExceptionUtils;
import com.kingsrook.qqq.backend.core.utils.Pair;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
@ -87,6 +114,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;
/*******************************************************************************
@ -96,9 +124,9 @@ public class ApiImplementation
{
private static final QLogger LOG = QLogger.getLogger(ApiImplementation.class);
/////////////////////////////////////
// key: Pair<apiName, apiVersion> //
/////////////////////////////////////
///////////////////////////////////////////////////////////////////
// key: Pair<apiName, apiVersion>, value: Map<name => metaData> //
///////////////////////////////////////////////////////////////////
private static Map<Pair<String, String>, Map<String, QTableMetaData>> tableApiNameMap = new HashMap<>();
@ -286,7 +314,7 @@ public class ApiImplementation
}
else
{
throw (new QBadRequestException("Request failed with " + badRequestMessages.size() + " reasons: " + StringUtils.join(" \n", badRequestMessages)));
throw (new QBadRequestException("Request failed with " + badRequestMessages.size() + " reasons: " + StringUtils.join("\n", badRequestMessages)));
}
}
@ -896,6 +924,291 @@ public class ApiImplementation
/*******************************************************************************
**
*******************************************************************************/
public static HttpApiResponse runProcess(ApiInstanceMetaData apiInstanceMetaData, String version, String processApiName, Map<String, String> paramMap) throws QException
{
Pair<ApiProcessMetaData, QProcessMetaData> pair = ApiProcessUtils.getProcessMetaDataPair(apiInstanceMetaData, version, processApiName);
ApiProcessMetaData apiProcessMetaData = pair.getA();
QProcessMetaData process = pair.getB();
String processName = process.getName();
List<String> badRequestMessages = new ArrayList<>();
String processUUID = UUID.randomUUID().toString();
RunProcessInput runProcessInput = new RunProcessInput();
runProcessInput.setProcessName(processName);
runProcessInput.setFrontendStepBehavior(RunProcessInput.FrontendStepBehavior.SKIP);
runProcessInput.setProcessUUID(processUUID);
// todo i don't think runProcessInput.setAsyncJobCallback();
//////////////////////
// map input values //
//////////////////////
ApiProcessInput apiProcessInput = apiProcessMetaData.getInput();
if(apiProcessInput != null)
{
processProcessInputFields(paramMap, badRequestMessages, runProcessInput, apiProcessInput.getQueryStringParams());
processProcessInputFields(paramMap, badRequestMessages, runProcessInput, apiProcessInput.getFormParams());
processProcessInputFields(paramMap, badRequestMessages, runProcessInput, apiProcessInput.getObjectBodyParams());
if(apiProcessInput.getBodyField() != null)
{
processSingleProcessInputField(apiProcessInput.getBodyField(), paramMap, badRequestMessages, runProcessInput);
}
}
////////////////////////////////////////
// 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 //
/////////////////////////////////////////
if(!badRequestMessages.isEmpty())
{
if(badRequestMessages.size() == 1)
{
throw (new QBadRequestException(badRequestMessages.get(0)));
}
else
{
throw (new QBadRequestException("Request failed with " + badRequestMessages.size() + " reasons: " + StringUtils.join("\n", badRequestMessages)));
}
}
/////////////////////////////////////////
// run pre-customizer, if there is one //
/////////////////////////////////////////
Map<String, QCodeReference> customizers = apiProcessMetaData.getCustomizers();
if(customizers != null && customizers.containsKey(ApiProcessCustomizers.PRE_RUN.getRole()))
{
PreRunApiProcessCustomizer preRunCustomizer = QCodeLoader.getAdHoc(PreRunApiProcessCustomizer.class, customizers.get(ApiProcessCustomizers.PRE_RUN.getRole()));
preRunCustomizer.preApiRun(runProcessInput);
}
boolean async = false;
if(ApiProcessMetaData.AsyncMode.ALWAYS.equals(apiProcessMetaData.getAsyncMode())
|| (ApiProcessMetaData.AsyncMode.OPTIONAL.equals(apiProcessMetaData.getAsyncMode()) && "true".equalsIgnoreCase(paramMap.get("async"))))
{
async = true;
}
if(async)
{
try
{
//////////////////////////////////////////////////////////////////////////////////////////////////////
// note - in other implementations, the process gets its own UUID (for process state to be stashed) //
// and the job gets its own (where we check in on running/complete). //
// but in this implementation, we want to just pass back one UUID to the caller, so make the job //
// manager use the process's uuid as the job uuid, and all will be revealed! //
//////////////////////////////////////////////////////////////////////////////////////////////////////
// todo? to help w/ StreamedETLPreview "should i count?" runProcessInput.setIsAsync(true);
new AsyncJobManager().withForcedJobUUID(processUUID).startJob(processName, 0, TimeUnit.MILLISECONDS, (callback) ->
{
runProcessInput.setAsyncJobCallback(callback);
return (new RunProcessAction().execute(runProcessInput));
});
}
catch(JobGoingAsyncException jgae)
{
LinkedHashMap<String, Object> response = new LinkedHashMap<>();
response.put("jobId", jgae.getJobUUID());
return (new HttpApiResponse(HttpStatus.Code.ACCEPTED, response));
}
////////////////////////////////////////////////////////////////////////////////////////
// passing 0 as the timeout to startJob *should* make it always throw the JGAE. But, //
// in case it didn't, we don't have a uuid to return to the caller, so that's a fail. //
////////////////////////////////////////////////////////////////////////////////////////
throw (new QException("Error starting asynchronous job - no job id was returned."));
}
/////////////////////
// run the process //
/////////////////////
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."));
}
catch(Exception e)
{
String concatenation = ExceptionUtils.concatenateMessagesFromChain(e);
throw (new QException(concatenation, e));
}
return (buildResponseAfterProcess(apiProcessMetaData, runProcessInput, runProcessOutput));
}
/*******************************************************************************
**
*******************************************************************************/
private static HttpApiResponse buildResponseAfterProcess(ApiProcessMetaData apiProcessMetaData, RunProcessInput runProcessInput, RunProcessOutput runProcessOutput) throws QException
{
//////////////////////////////////////////
// run post-customizer, if there is one //
//////////////////////////////////////////
Map<String, QCodeReference> customizers = apiProcessMetaData.getCustomizers();
if(customizers != null && customizers.containsKey(ApiProcessCustomizers.POST_RUN.getRole()))
{
PostRunApiProcessCustomizer postRunCustomizer = QCodeLoader.getAdHoc(PostRunApiProcessCustomizer.class, customizers.get(ApiProcessCustomizers.POST_RUN.getRole()));
postRunCustomizer.postApiRun(runProcessInput, runProcessOutput);
}
///////////////////////
// map output values //
///////////////////////
ApiProcessOutputInterface output = apiProcessMetaData.getOutput();
if(output != null)
{
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;
}
for(QFieldMetaData inputField : CollectionUtils.nonNullList(fieldsContainer.getFields()))
{
processSingleProcessInputField(inputField, paramMap, badRequestMessages, runProcessInput);
}
}
/*******************************************************************************
**
*******************************************************************************/
private static void processSingleProcessInputField(QFieldMetaData inputField, Map<String, String> paramMap, List<String> badRequestMessages, RunProcessInput runProcessInput)
{
String value = paramMap.get(inputField.getName());
if(!StringUtils.hasContent(value) && inputField.getDefaultValue() != null)
{
value = ValueUtils.getValueAsString(inputField.getDefaultValue());
}
if(!StringUtils.hasContent(value) && inputField.getIsRequired())
{
badRequestMessages.add("Missing value for required input field " + inputField.getName());
return;
}
// todo - types?
runProcessInput.addValue(inputField.getName(), value);
}
/*******************************************************************************
**
*******************************************************************************/
public static HttpApiResponse getProcessStatus(ApiInstanceMetaData apiInstanceMetaData, String version, String apiProcessName, String jobUUID) throws QException
{
Optional<AsyncJobStatus> optionalJobStatus = new AsyncJobManager().getJobStatus(jobUUID);
if(optionalJobStatus.isEmpty())
{
throw (new QException("Could not find status of process job: " + jobUUID));
}
AsyncJobStatus jobStatus = optionalJobStatus.get();
// resultForCaller.put("jobStatus", jobStatus);
LOG.debug("Job status is " + jobStatus.getState() + " for " + jobUUID);
if(jobStatus.getState().equals(AsyncJobState.COMPLETE))
{
///////////////////////////////////////////////////////////////////////////////////////
// if the job is complete, get the process result from state provider, and return it //
// this output should look like it did if the job finished synchronously!! //
///////////////////////////////////////////////////////////////////////////////////////
Optional<ProcessState> processState = RunProcessAction.getState(jobUUID);
if(processState.isPresent())
{
RunProcessOutput runProcessOutput = new RunProcessOutput(processState.get());
RunProcessInput runProcessInput = new RunProcessInput();
runProcessInput.seedFromProcessState(processState.get());
Pair<ApiProcessMetaData, QProcessMetaData> pair = ApiProcessUtils.getProcessMetaDataPair(apiInstanceMetaData, version, apiProcessName);
ApiProcessMetaData apiProcessMetaData = pair.getA();
return (buildResponseAfterProcess(apiProcessMetaData, runProcessInput, runProcessOutput));
}
else
{
throw (new QException("Could not find results for completed of process job: " + jobUUID));
}
}
else if(jobStatus.getState().equals(AsyncJobState.ERROR))
{
///////////////////////////////////////////////////////////////////////////////////////////////////////////
// if the job had an error (e.g., a process step threw), "nicely" serialize its exception for the caller //
///////////////////////////////////////////////////////////////////////////////////////////////////////////
if(jobStatus.getCaughtException() != null)
{
throw (new QException(jobStatus.getCaughtException()));
}
else
{
throw (new QException("Job failed with an unspecified error."));
}
}
else
{
LinkedHashMap<String, Object> response = new LinkedHashMap<>();
response.put("jobId", jobUUID);
response.put("message", jobStatus.getMessage());
if(jobStatus.getCurrent() != null && jobStatus.getTotal() != null)
{
response.put("current", jobStatus.getCurrent());
response.put("total", jobStatus.getTotal());
}
return (new HttpApiResponse(HttpStatus.Code.ACCEPTED, response));
}
}
/*******************************************************************************
**
*******************************************************************************/
@ -1078,14 +1391,14 @@ public class ApiImplementation
ApiTableMetaDataContainer apiTableMetaDataContainer = ApiTableMetaDataContainer.of(table);
if(apiTableMetaDataContainer == null)
{
LOG.info("404 because table apiMetaDataContainer is null", logPairs);
LOG.info("404 because table apiTableMetaDataContainer is null", logPairs);
throw (new QNotFoundException("Could not find a table named " + tableApiName + " in this api."));
}
ApiTableMetaData apiTableMetaData = apiTableMetaDataContainer.getApiTableMetaData(apiInstanceMetaData.getName());
if(apiTableMetaData == null)
{
LOG.info("404 because table apiMetaData is null", logPairs);
LOG.info("404 because table apiTableMetaData is null", logPairs);
throw (new QNotFoundException("Could not find a table named " + tableApiName + " in this api."));
}
@ -1185,4 +1498,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,13 @@ 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.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.ApiProcessOutputInterface;
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,16 +80,19 @@ 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;
import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder;
import io.javalin.http.ContentType;
import io.javalin.http.HttpStatus;
import org.apache.commons.lang.BooleanUtils;
@ -217,7 +227,8 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
throw new QException("Missing required input: version");
}
if(!apiInstanceMetaData.getSupportedVersions().contains(new APIVersion(version)))
APIVersion apiVersion = new APIVersion(version);
if(!apiInstanceMetaData.getSupportedVersions().contains(apiVersion))
{
throw (new QException("[" + version + "] is not a supported API Version."));
}
@ -288,9 +299,12 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
.withDescription("Requested result page size")
)));
///////////////////
// foreach table //
///////////////////
List<Tag> tagList = new ArrayList<>();
Set<String> usedProcessNames = new HashSet<>();
/////////////////////////////////////
// foreach table (sorted by label) //
/////////////////////////////////////
List<QTableMetaData> tables = new ArrayList<>(qInstance.getTables().values());
tables.sort(Comparator.comparing(t -> ObjectUtils.requireNonNullElse(t.getLabel(), t.getName(), "")));
for(QTableMetaData table : tables)
@ -330,7 +344,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 +369,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;
}
@ -374,6 +390,10 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
String primaryKeyApiName = ApiFieldMetaData.getEffectiveApiFieldName(apiName, primaryKeyField);
List<QFieldMetaData> tableApiFields = new GetTableApiFieldsAction().execute(new GetTableApiFieldsInput().withTableName(tableName).withVersion(version).withApiName(apiName)).getFields();
tagList.add(new Tag()
.withName(tableLabel)
.withDescription("Operations on the " + tableLabel + " table."));
///////////////////////////////
// permissions for the table //
///////////////////////////////
@ -405,13 +425,6 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
// todo - handle non read/edit/insert/delete tables (e.g., w/ just 1 permission, or read/write) //
//////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////
// tag for this table //
////////////////////////
openAPI.getTags().add(new Tag()
.withName(tableLabel)
.withDescription("Operations on the " + tableLabel + " table."));
//////////////////////////////////////
// build the schemas for this table //
//////////////////////////////////////
@ -687,8 +700,65 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
.withPatch(updateBulkEnabled ? bulkPatch : null)
.withDelete(deleteBulkEnabled ? bulkDelete : null));
}
///////////////////////////////////////
// add processes under the table //
// do we want a unique tag for them? //
///////////////////////////////////////
/*
String tableProcessesTag = tableLabel + " Processes";
if(CollectionUtils.nullSafeHasContents(apiProcessMetaDataList))
{
tagList.add(new Tag()
.withName(tableProcessesTag)
.withDescription("Process on the " + tableLabel + " table."));
}
*/
String tableProcessesTag = tableLabel;
for(Pair<ApiProcessMetaData, QProcessMetaData> pair : CollectionUtils.nonNullList(apiProcessMetaDataList))
{
ApiProcessMetaData apiProcessMetaData = pair.getA();
QProcessMetaData processMetaData = pair.getB();
addProcessEndpoints(qInstance, apiInstanceMetaData, basePath, openAPI, tableProcessesTag, apiProcessMetaData, processMetaData);
usedProcessNames.add(processMetaData.getName());
}
}
/////////////////////////////
// add non-table processes //
/////////////////////////////
if(input.getTableName() == null)
{
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 tag = processMetaData.getLabel();
if(doesProcessLabelNeedTheWordProcessAppended(tag))
{
tag += " process";
}
tagList.add(new Tag()
.withName(tag)
.withDescription(tag));
addProcessEndpoints(qInstance, apiInstanceMetaData, basePath, openAPI, tag, apiProcessMetaData, processMetaData);
usedProcessNames.add(processMetaData.getName());
}
}
tagList.sort(Comparator.comparing(Tag::getName));
openAPI.setTags(tagList);
////////////////////////////
// define standard errors //
////////////////////////////
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\""));
componentResponses.put("error" + HttpStatus.UNAUTHORIZED.getCode(), buildStandardErrorResponse("Unauthorized. The required authentication credentials were missing or invalid.", "The required authentication credentials were missing or invalid."));
componentResponses.put("error" + HttpStatus.FORBIDDEN.getCode(), buildStandardErrorResponse("Forbidden. You do not have permission to access the requested resource.", "You do not have permission to access the requested resource."));
@ -710,6 +780,353 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
/*******************************************************************************
**
*******************************************************************************/
private static boolean doesProcessLabelNeedTheWordProcessAppended(String tag)
{
return !tag.matches("(?i).* process$");
}
/*******************************************************************************
**
*******************************************************************************/
private void addProcessEndpoints(QInstance qInstance, ApiInstanceMetaData apiInstanceMetaData, String basePath, OpenAPI openAPI, String tag, ApiProcessMetaData apiProcessMetaData, QProcessMetaData processMetaData)
{
String processApiPath = ApiProcessUtils.getProcessApiPath(qInstance, processMetaData, apiProcessMetaData, apiInstanceMetaData);
///////////////////////////
// do the process itself //
///////////////////////////
Path path = generateProcessSpecPathObject(apiInstanceMetaData, apiProcessMetaData, processMetaData, ListBuilder.of(tag));
openAPI.getPaths().put(basePath + processApiPath, path);
///////////////////////////////////////////////////////////////////////
// if the process can run async, then do the status checkin endpoitn //
///////////////////////////////////////////////////////////////////////
if(!ApiProcessMetaData.AsyncMode.NEVER.equals(apiProcessMetaData.getAsyncMode()))
{
Path statusPath = generateProcessStatusSpecPathObject(apiInstanceMetaData, apiProcessMetaData, processMetaData, ListBuilder.of(tag));
openAPI.getPaths().put(basePath + processApiPath + "/status/{jobId}", statusPath);
}
}
/*******************************************************************************
**
*******************************************************************************/
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)
{
String description = apiProcessMetaData.getDescription();
if(!StringUtils.hasContent(description))
{
description = "Run the " + processMetaData.getLabel();
if(doesProcessLabelNeedTheWordProcessAppended(description))
{
description += " process";
}
}
////////////////////////////////
// start defining the process //
////////////////////////////////
Method methodForProcess = new Method()
.withOperationId(apiProcessMetaData.getApiProcessName())
.withTags(tags)
.withSummary(ObjectUtils.requireConditionElse(apiProcessMetaData.getSummary(), StringUtils::hasContent, processMetaData.getLabel()))
.withDescription(description)
.withSecurity(getSecurity(apiInstanceMetaData, processMetaData.getName()));
////////////////////////////////
// add inputs for the process //
////////////////////////////////
List<Parameter> parameters = new ArrayList<>();
ApiProcessInput apiProcessInput = apiProcessMetaData.getInput();
if(apiProcessInput != null)
{
ApiProcessInputFieldsContainer queryStringParams = apiProcessInput.getQueryStringParams();
if(queryStringParams != null)
{
if(queryStringParams.getRecordIdsField() != null)
{
parameters.add(processFieldToParameter(apiInstanceMetaData, queryStringParams.getRecordIdsField()).withIn("query"));
}
for(QFieldMetaData field : CollectionUtils.nonNullList(queryStringParams.getFields()))
{
parameters.add(processFieldToParameter(apiInstanceMetaData, field).withIn("query"));
}
}
QFieldMetaData bodyField = apiProcessInput.getBodyField();
if(bodyField != null)
{
ApiFieldMetaDataContainer apiFieldMetaDataContainer = ApiFieldMetaDataContainer.ofOrNew(bodyField);
ApiFieldMetaData apiFieldMetaData = apiFieldMetaDataContainer.getApiFieldMetaData(apiInstanceMetaData.getName());
String bodyDescription = "Value for the " + bodyField.getLabel();
if(apiFieldMetaData != null && StringUtils.hasContent(apiFieldMetaData.getDescription()))
{
bodyDescription = apiFieldMetaData.getDescription();
}
Content content = new Content();
if(apiFieldMetaData != null && apiFieldMetaData.getExample() instanceof ExampleWithSingleValue exampleWithSingleValue)
{
content.withSchema(new Schema()
.withDescription(bodyDescription)
.withType("string")
.withExample(exampleWithSingleValue.getValue())
);
}
methodForProcess.withRequestBody(new RequestBody()
.withDescription(bodyDescription)
.withRequired(bodyField.getIsRequired())
.withContent(MapBuilder.of(apiProcessInput.getBodyFieldContentType(), content)));
}
// todo - form & record body params
// todo methodForProcess.withRequestBody();
}
////////////////////////////////////////////////////////
// add the async input for optionally-async processes //
////////////////////////////////////////////////////////
if(ApiProcessMetaData.AsyncMode.OPTIONAL.equals(apiProcessMetaData.getAsyncMode()))
{
parameters.add(new Parameter()
.withName("async")
.withIn("query")
.withDescription("""
Indicates if the job should be ran asynchronously.
If false, or not specified, job is ran synchronously, and returns with response status of 207 (Multi-Status) or 204 (No Content).
If true, request returns immediately with response status of 202 (Accepted).
""")
.withExamples(MapBuilder.of(
"false", new ExampleWithSingleValue().withValue(false).withSummary("Run the job synchronously."),
"true", new ExampleWithSingleValue().withValue(true).withSummary("Run the job asynchronously.")
))
.withSchema(new Schema().withType("boolean")));
}
if(CollectionUtils.nullSafeHasContents(parameters))
{
methodForProcess.setParameters(parameters);
}
//////////////////////////////////
// build all possible responses //
//////////////////////////////////
Map<Integer, Response> responses = new LinkedHashMap<>();
ApiProcessOutputInterface output = apiProcessMetaData.getOutput();
if(!ApiProcessMetaData.AsyncMode.ALWAYS.equals(apiProcessMetaData.getAsyncMode()))
{
responses.putAll(output.getSpecResponses(apiInstanceMetaData.getName()));
}
if(!ApiProcessMetaData.AsyncMode.NEVER.equals(apiProcessMetaData.getAsyncMode()))
{
responses.put(HttpStatus.ACCEPTED.getCode(), new Response()
.withDescription("The process has been started asynchronously. You can call back later to check its status.")
.withContent(MapBuilder.of(ContentType.JSON, new Content()
.withSchema(new Schema()
.withType("object")
.withProperties(MapBuilder.of(
"jobId", new Schema().withType("string").withFormat("uuid").withDescription("id of the asynchronous job")
))
)
))
);
}
responses.putAll(buildStandardErrorResponses(apiInstanceMetaData));
methodForProcess.withResponses(responses);
@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 Path generateProcessStatusSpecPathObject(ApiInstanceMetaData apiInstanceMetaData, ApiProcessMetaData apiProcessMetaData, QProcessMetaData processMetaData, List<String> tags)
{
////////////////////////////////
// start defining the process //
////////////////////////////////
Method methodForProcess = new Method()
.withOperationId("getStatusFor" + StringUtils.ucFirst(apiProcessMetaData.getApiProcessName()))
.withTags(tags)
.withSummary("Get Status of Job: " + ObjectUtils.requireConditionElse(apiProcessMetaData.getSummary(), StringUtils::hasContent, processMetaData.getLabel()))
.withDescription("Get the status for a previous asynchronous call to the process named " + processMetaData.getLabel())
.withSecurity(getSecurity(apiInstanceMetaData, processMetaData.getName()));
////////////////////////////////////////////////////////
// add the async input for optionally-async processes //
////////////////////////////////////////////////////////
methodForProcess.setParameters(ListBuilder.of(new Parameter()
.withName("jobId")
.withIn("path")
.withRequired(true)
.withDescription("Id of the job, as returned by the API call that started it.")
.withSchema(new Schema().withType("string").withFormat("uuid"))
));
//////////////////////////////////
// build all possible responses //
//////////////////////////////////
Map<Integer, Response> responses = new LinkedHashMap<>();
responses.put(HttpStatus.ACCEPTED.getCode(), new Response()
.withDescription("The process is still running. You can call back later to get its final status.")
.withContent(MapBuilder.of(ContentType.JSON, new Content()
.withSchema(new Schema()
.withType("object")
.withProperties(MapBuilder.of(
"jobId", new Schema().withType("string").withFormat("uuid").withDescription("id of the asynchronous job")
// todo - status??
))
)
))
);
ApiProcessOutputInterface output = apiProcessMetaData.getOutput();
responses.putAll(output.getSpecResponses(apiInstanceMetaData.getName()));
responses.putAll(buildStandardErrorResponses(apiInstanceMetaData));
methodForProcess.withResponses(responses);
return (new Path().withGet(methodForProcess));
}
/*******************************************************************************
**
*******************************************************************************/
private Parameter processFieldToParameter(ApiInstanceMetaData apiInstanceMetaData, QFieldMetaData field)
{
ApiFieldMetaDataContainer apiFieldMetaDataContainer = ApiFieldMetaDataContainer.ofOrNew(field);
ApiFieldMetaData apiFieldMetaData = apiFieldMetaDataContainer.getApiFieldMetaData(apiInstanceMetaData.getName());
String description = "Value for the " + field.getLabel() + " field.";
if(field.getDefaultValue() != null)
{
description += " Default value is " + field.getDefaultValue() + ", if not given.";
}
Schema fieldSchema = getFieldSchema(field, description, apiInstanceMetaData);
Parameter parameter = new Parameter()
.withName(field.getName())
.withDescription(description)
.withRequired(field.getIsRequired())
.withSchema(fieldSchema);
if(apiFieldMetaData != null)
{
if(apiFieldMetaData.getExample() != null)
{
parameter.withExample(apiFieldMetaData.getExample());
}
else if(apiFieldMetaData.getExamples() != null)
{
parameter.withExamples(apiFieldMetaData.getExamples());
}
}
return (parameter);
}
/*******************************************************************************
**
*******************************************************************************/
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
@ -766,7 +1183,14 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
for(QFieldMetaData field : tableApiFields)
{
Schema fieldSchema = getFieldSchema(table, field);
String fieldLabel = field.getLabel();
if(!StringUtils.hasContent(fieldLabel))
{
fieldLabel = QInstanceEnricher.nameToLabel(field.getName());
}
String defaultDescription = fieldLabel + " for the " + table.getLabel() + ".";
Schema fieldSchema = getFieldSchema(field, defaultDescription, apiInstanceMetaData);
tableFields.put(ApiFieldMetaData.getEffectiveApiFieldName(apiInstanceMetaData.getName(), field), fieldSchema);
}
@ -937,20 +1361,22 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
/*******************************************************************************
**
*******************************************************************************/
private Schema getFieldSchema(QTableMetaData table, QFieldMetaData field)
private Schema getFieldSchema(QFieldMetaData field, String defaultDescription, ApiInstanceMetaData apiInstanceMetaData)
{
String fieldLabel = field.getLabel();
if(!StringUtils.hasContent(fieldLabel))
{
fieldLabel = QInstanceEnricher.nameToLabel(field.getName());
}
ApiFieldMetaDataContainer apiFieldMetaDataContainer = ApiFieldMetaDataContainer.ofOrNew(field);
ApiFieldMetaData apiFieldMetaData = apiFieldMetaDataContainer.getApiFieldMetaData(apiInstanceMetaData.getName());
String description = fieldLabel + " for the " + table.getLabel() + ".";
String description = defaultDescription;
if(field.getType().equals(QFieldType.BLOB))
{
description = "Base64 encoded " + description;
}
if(apiFieldMetaData != null && StringUtils.hasContent(apiFieldMetaData.getDescription()))
{
description = apiFieldMetaData.getDescription();
}
Schema fieldSchema = new Schema()
.withType(getFieldType(field))
.withFormat(getFieldFormat(field))
@ -1163,7 +1589,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
/*******************************************************************************
**
*******************************************************************************/
private String getFieldType(QFieldMetaData field)
public static String getFieldType(QFieldMetaData field)
{
return (getFieldType(field.getType()));
}
@ -1174,7 +1600,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
**
*******************************************************************************/
@SuppressWarnings("checkstyle:indentation")
private String getFieldType(QFieldType type)
private static String getFieldType(QFieldType type)
{
return switch(type)
{

View File

@ -28,10 +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;
@ -40,11 +43,18 @@ 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;
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
import com.kingsrook.qqq.backend.core.context.QContext;
@ -54,6 +64,7 @@ import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QModuleDispatchException;
import com.kingsrook.qqq.backend.core.exceptions.QNotFoundException;
import com.kingsrook.qqq.backend.core.exceptions.QPermissionDeniedException;
import com.kingsrook.qqq.backend.core.exceptions.QRuntimeException;
import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
@ -66,6 +77,8 @@ import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.authentication.Auth0AuthenticationMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.branding.QBrandingMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.model.session.QUser;
@ -74,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;
@ -85,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;
@ -96,6 +111,8 @@ public class QJavalinApiHandler
{
private static final QLogger LOG = QLogger.getLogger(QJavalinApiHandler.class);
private static final ApiProcessMetaDataContainer EMPTY_API_PROCESS_META_DATA_CONTAINER = new ApiProcessMetaDataContainer().withApis(Collections.emptyMap());
private static QInstance qInstance;
private static Map<String, Integer> apiLogUserIdCache = new HashMap<>();
@ -156,10 +173,47 @@ public class QJavalinApiHandler
////////////////////////////////////////////
ApiBuilder.get("/", context -> doSpecHtml(context, apiInstanceMetaData));
///////////////////////////////////////////
// add known paths for specs & docs page //
///////////////////////////////////////////
ApiBuilder.get("/openapi.yaml", context -> doSpecYaml(context, apiInstanceMetaData));
ApiBuilder.get("/openapi.json", context -> doSpecJson(context, apiInstanceMetaData));
ApiBuilder.get("/openapi.html", context -> doSpecHtml(context, apiInstanceMetaData));
///////////////////
// add processes //
///////////////////
for(QProcessMetaData process : qInstance.getProcesses().values())
{
ApiProcessMetaDataContainer apiProcessMetaDataContainer = Objects.requireNonNullElse(ApiProcessMetaDataContainer.of(process), EMPTY_API_PROCESS_META_DATA_CONTAINER);
ApiProcessMetaData apiProcessMetaData = apiProcessMetaDataContainer.getApis().get(apiInstanceMetaData.getName());
if(apiProcessMetaData != null && !BooleanUtils.isTrue(apiProcessMetaData.getIsExcluded()))
{
String path = ApiProcessUtils.getProcessApiPath(qInstance, process, apiProcessMetaData, apiInstanceMetaData);
HttpMethod method = apiProcessMetaData.getMethod();
switch(method)
{
case GET -> ApiBuilder.get(path, context -> runProcess(context, process, apiProcessMetaData, apiInstanceMetaData));
case POST -> ApiBuilder.post(path, context -> runProcess(context, process, apiProcessMetaData, apiInstanceMetaData));
case PUT -> ApiBuilder.put(path, context -> runProcess(context, process, apiProcessMetaData, apiInstanceMetaData));
case PATCH -> ApiBuilder.patch(path, context -> runProcess(context, process, apiProcessMetaData, apiInstanceMetaData));
case DELETE -> ApiBuilder.delete(path, context -> runProcess(context, process, apiProcessMetaData, apiInstanceMetaData));
default -> throw (new QRuntimeException("Unrecognized http method [" + method + "] for process [" + process.getName() + "]"));
}
make405sForOtherMethods(method, path);
if(!ApiProcessMetaData.AsyncMode.NEVER.equals(apiProcessMetaData.getAsyncMode()))
{
ApiBuilder.get(path + "/status/{jobId}", context -> getProcessStatus(context, process, apiProcessMetaData, apiInstanceMetaData));
}
}
}
///////////////////////////////////
// add wildcard paths for tables //
///////////////////////////////////
ApiBuilder.path("/{tableName}", () ->
{
ApiBuilder.get("/openapi.yaml", context -> doSpecYaml(context, apiInstanceMetaData));
@ -208,6 +262,171 @@ 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
}
/*******************************************************************************
**
*******************************************************************************/
@SuppressWarnings("checkstyle:indentation")
private void runProcess(Context context, QProcessMetaData processMetaData, ApiProcessMetaData apiProcessMetaData, ApiInstanceMetaData apiInstanceMetaData)
{
String version = context.pathParam("version");
APILog apiLog = newAPILog(context);
try
{
setupSession(context, null, version, apiInstanceMetaData);
QJavalinAccessLogger.logStart("apiRunProcess", logPair("process", processMetaData.getName()));
////////////////////////////////////////////////////
// process inputs into map for api implementation //
////////////////////////////////////////////////////
Map<String, String> parameters = new LinkedHashMap<>();
ApiProcessInput input = apiProcessMetaData.getInput();
if(input != null)
{
processProcessInputFieldsContainer(context, parameters, input.getQueryStringParams(), Context::queryParam);
processProcessInputFieldsContainer(context, parameters, input.getFormParams(), Context::formParam);
ApiProcessInputFieldsContainer objectBodyParams = input.getObjectBodyParams();
if(objectBodyParams != null)
{
JSONObject jsonObject = new JSONObject(context.body());
processProcessInputFieldsContainer(context, parameters, objectBodyParams, (ctx, name) -> jsonObject.optString(name, null));
}
if(input.getBodyField() != null)
{
parameters.put(input.getBodyField().getName(), context.body());
}
}
if(ApiProcessMetaData.AsyncMode.OPTIONAL.equals(apiProcessMetaData.getAsyncMode()))
{
parameters.put("async", context.queryParam("async"));
}
/////////////////////
// run the process //
/////////////////////
HttpApiResponse response = ApiImplementation.runProcess(apiInstanceMetaData, version, apiProcessMetaData.getApiProcessName(), parameters);
//////////////////
// log & return //
//////////////////
QJavalinAccessLogger.logEndSuccess();
context.status(response.getStatusCode().getCode());
String resultString = toJson(Objects.requireNonNullElse(response.getResponseBodyObject(), ""));
context.result(resultString);
storeApiLog(apiLog.withStatusCode(context.statusCode()).withResponseBody(resultString));
}
catch(Exception e)
{
QJavalinAccessLogger.logEndFail(e);
handleException(context, e, apiLog);
}
}
/*******************************************************************************
**
*******************************************************************************/
private void getProcessStatus(Context context, QProcessMetaData processMetaData, ApiProcessMetaData apiProcessMetaData, ApiInstanceMetaData apiInstanceMetaData)
{
String version = context.pathParam("version");
APILog apiLog = newAPILog(context);
try
{
setupSession(context, null, version, apiInstanceMetaData);
QJavalinAccessLogger.logStart("apiGetProcessStatus", logPair("process", processMetaData.getName()));
String jobId = context.pathParam("jobId");
HttpApiResponse response = ApiImplementation.getProcessStatus(apiInstanceMetaData, version, apiProcessMetaData.getApiProcessName(), jobId);
//////////////////
// log & return //
//////////////////
QJavalinAccessLogger.logEndSuccess();
context.status(response.getStatusCode().getCode());
String resultString = toJson(Objects.requireNonNullElse(response.getResponseBodyObject(), ""));
context.result(resultString);
storeApiLog(apiLog.withStatusCode(context.statusCode()).withResponseBody(resultString));
}
catch(Exception e)
{
QJavalinAccessLogger.logEndFail(e);
handleException(context, e, apiLog);
}
}
/*******************************************************************************
**
*******************************************************************************/
private static void processProcessInputFieldsContainer(Context context, Map<String, String> parameters, ApiProcessInputFieldsContainer fieldsContainer, BiFunction<Context, String, String> paramAccessor)
{
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);
}
}
}
}
/*******************************************************************************
**
*******************************************************************************/
@ -1161,7 +1380,12 @@ public class QJavalinApiHandler
// default exception handling //
////////////////////////////////
LOG.warn("Exception in javalin request", e);
respondWithError(context, HttpStatus.Code.INTERNAL_SERVER_ERROR, e.getClass().getSimpleName() + " (" + e.getMessage() + ")", apiLog); // 500
String message = e.getMessage();
if(!StringUtils.hasContent(message))
{
message = e.getClass().getSimpleName();
}
respondWithError(context, HttpStatus.Code.INTERNAL_SERVER_ERROR, message, apiLog); // 500
return;
}
}

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

@ -24,17 +24,17 @@ package com.kingsrook.qqq.api.model.metadata;
import java.util.LinkedHashMap;
import java.util.Map;
import com.kingsrook.qqq.api.ApiMiddlewareType;
import com.kingsrook.qqq.api.ApiSupplementType;
import com.kingsrook.qqq.backend.core.instances.QInstanceValidator;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.QMiddlewareInstanceMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.QSupplementalInstanceMetaData;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
/*******************************************************************************
**
*******************************************************************************/
public class ApiInstanceMetaDataContainer extends QMiddlewareInstanceMetaData
public class ApiInstanceMetaDataContainer extends QSupplementalInstanceMetaData
{
private Map<String, ApiInstanceMetaData> apis;
@ -46,7 +46,7 @@ public class ApiInstanceMetaDataContainer extends QMiddlewareInstanceMetaData
*******************************************************************************/
public ApiInstanceMetaDataContainer()
{
setType(ApiMiddlewareType.NAME);
setType(ApiSupplementType.NAME);
}
@ -56,7 +56,7 @@ public class ApiInstanceMetaDataContainer extends QMiddlewareInstanceMetaData
*******************************************************************************/
public static ApiInstanceMetaDataContainer of(QInstance qInstance)
{
return ((ApiInstanceMetaDataContainer) qInstance.getMiddlewareMetaData(ApiMiddlewareType.NAME));
return ((ApiInstanceMetaDataContainer) qInstance.getSupplementalMetaData(ApiSupplementType.NAME));
}

View File

@ -22,6 +22,8 @@
package com.kingsrook.qqq.api.model.metadata.fields;
import java.util.Map;
import com.kingsrook.qqq.api.model.openapi.Example;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
@ -35,10 +37,14 @@ public class ApiFieldMetaData
private String finalVersion;
private String apiFieldName;
private String description;
private Boolean isExcluded;
private String replacedByFieldName;
private Example example;
private Map<String, Example> examples;
/*******************************************************************************
@ -214,4 +220,97 @@ public class ApiFieldMetaData
return (this);
}
/*******************************************************************************
** Getter for description
*******************************************************************************/
public String getDescription()
{
return (this.description);
}
/*******************************************************************************
** Setter for description
*******************************************************************************/
public void setDescription(String description)
{
this.description = description;
}
/*******************************************************************************
** Fluent setter for description
*******************************************************************************/
public ApiFieldMetaData withDescription(String description)
{
this.description = description;
return (this);
}
/*******************************************************************************
** Getter for example
*******************************************************************************/
public Example getExample()
{
return (this.example);
}
/*******************************************************************************
** Setter for example
*******************************************************************************/
public void setExample(Example example)
{
this.example = example;
}
/*******************************************************************************
** Fluent setter for example
*******************************************************************************/
public ApiFieldMetaData withExample(Example example)
{
this.example = example;
return (this);
}
/*******************************************************************************
** Getter for examples
*******************************************************************************/
public Map<String, Example> getExamples()
{
return (this.examples);
}
/*******************************************************************************
** Setter for examples
*******************************************************************************/
public void setExamples(Map<String, Example> examples)
{
this.examples = examples;
}
/*******************************************************************************
** Fluent setter for examples
*******************************************************************************/
public ApiFieldMetaData withExamples(Map<String, Example> examples)
{
this.examples = examples;
return (this);
}
}

View File

@ -24,18 +24,20 @@ package com.kingsrook.qqq.api.model.metadata.fields;
import java.util.LinkedHashMap;
import java.util.Map;
import com.kingsrook.qqq.api.ApiMiddlewareType;
import java.util.Objects;
import com.kingsrook.qqq.api.ApiSupplementType;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QMiddlewareFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QSupplementalFieldMetaData;
/*******************************************************************************
**
*******************************************************************************/
public class ApiFieldMetaDataContainer extends QMiddlewareFieldMetaData
public class ApiFieldMetaDataContainer extends QSupplementalFieldMetaData
{
private Map<String, ApiFieldMetaData> apis;
private ApiFieldMetaData defaultApiFieldMetaData;
/*******************************************************************************
@ -54,7 +56,18 @@ public class ApiFieldMetaDataContainer extends QMiddlewareFieldMetaData
*******************************************************************************/
public static ApiFieldMetaDataContainer of(QFieldMetaData field)
{
return ((ApiFieldMetaDataContainer) field.getMiddlewareMetaData(ApiMiddlewareType.NAME));
return ((ApiFieldMetaDataContainer) field.getSupplementalMetaData(ApiSupplementType.NAME));
}
/*******************************************************************************
** either get the container attached to a field - or a new one - note - the new
** one will NOT be attached to the field!!
*******************************************************************************/
public static ApiFieldMetaDataContainer ofOrNew(QFieldMetaData field)
{
return (Objects.requireNonNullElseGet(of(field), ApiFieldMetaDataContainer::new));
}
@ -70,16 +83,16 @@ public class ApiFieldMetaDataContainer extends QMiddlewareFieldMetaData
/*******************************************************************************
** Getter for apis
** Getter the apiFieldMetaData for a specific api, or the container's default
*******************************************************************************/
public ApiFieldMetaData getApiFieldMetaData(String apiName)
{
if(this.apis == null)
{
return (null);
return (defaultApiFieldMetaData);
}
return (this.apis.get(apiName));
return (this.apis.getOrDefault(apiName, defaultApiFieldMetaData));
}
@ -118,4 +131,35 @@ public class ApiFieldMetaDataContainer extends QMiddlewareFieldMetaData
return (this);
}
/*******************************************************************************
** Getter for defaultApiFieldMetaData
*******************************************************************************/
public ApiFieldMetaData getDefaultApiFieldMetaData()
{
return (this.defaultApiFieldMetaData);
}
/*******************************************************************************
** Setter for defaultApiFieldMetaData
*******************************************************************************/
public void setDefaultApiFieldMetaData(ApiFieldMetaData defaultApiFieldMetaData)
{
this.defaultApiFieldMetaData = defaultApiFieldMetaData;
}
/*******************************************************************************
** Fluent setter for defaultApiFieldMetaData
*******************************************************************************/
public ApiFieldMetaDataContainer withDefaultApiFieldMetaData(ApiFieldMetaData defaultApiFieldMetaData)
{
this.defaultApiFieldMetaData = defaultApiFieldMetaData;
return (this);
}
}

View File

@ -0,0 +1,87 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.api.model.metadata.processes;
/*******************************************************************************
**
*******************************************************************************/
public enum ApiProcessCustomizers
{
PRE_RUN("preRun", PreRunApiProcessCustomizer.class),
POST_RUN("postRun", PreRunApiProcessCustomizer.class);
private final String role;
private final Class<?> expectedType;
/*******************************************************************************
**
*******************************************************************************/
ApiProcessCustomizers(String role, Class<?> expectedType)
{
this.role = role;
this.expectedType = expectedType;
}
/*******************************************************************************
** Get the FilesystemTableCustomer for a given role (e.g., the role used in meta-data, not
** the enum-constant name).
*******************************************************************************/
public static ApiProcessCustomizers forRole(String name)
{
for(ApiProcessCustomizers value : values())
{
if(value.role.equals(name))
{
return (value);
}
}
return (null);
}
/*******************************************************************************
** Getter for role
**
*******************************************************************************/
public String getRole()
{
return role;
}
/*******************************************************************************
** Getter for expectedType
**
*******************************************************************************/
public Class<?> getExpectedType()
{
return expectedType;
}
}

View File

@ -0,0 +1,220 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.api.model.metadata.processes;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
/*******************************************************************************
**
*******************************************************************************/
public class ApiProcessInput
{
private ApiProcessInputFieldsContainer queryStringParams;
private ApiProcessInputFieldsContainer formParams;
private ApiProcessInputFieldsContainer recordBodyParams;
private QFieldMetaData bodyField;
private String bodyFieldContentType;
/*******************************************************************************
**
*******************************************************************************/
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);
}
/*******************************************************************************
** Getter for bodyField
*******************************************************************************/
public QFieldMetaData getBodyField()
{
return (this.bodyField);
}
/*******************************************************************************
** Setter for bodyField
*******************************************************************************/
public void setBodyField(QFieldMetaData bodyField)
{
this.bodyField = bodyField;
}
/*******************************************************************************
** Fluent setter for bodyField
*******************************************************************************/
public ApiProcessInput withBodyField(QFieldMetaData bodyField)
{
this.bodyField = bodyField;
return (this);
}
/*******************************************************************************
** Getter for bodyFieldContentType
*******************************************************************************/
public String getBodyFieldContentType()
{
return (this.bodyFieldContentType);
}
/*******************************************************************************
** Setter for bodyFieldContentType
*******************************************************************************/
public void setBodyFieldContentType(String bodyFieldContentType)
{
this.bodyFieldContentType = bodyFieldContentType;
}
/*******************************************************************************
** Fluent setter for bodyFieldContentType
*******************************************************************************/
public ApiProcessInput withBodyFieldContentType(String bodyFieldContentType)
{
this.bodyFieldContentType = bodyFieldContentType;
return (this);
}
}

View File

@ -0,0 +1,161 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.api.model.metadata.processes;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
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;
/*******************************************************************************
** find all input fields in frontend steps of the process, and add them as fields
** in this container.
*******************************************************************************/
public ApiProcessInputFieldsContainer withInferredInputFields(QProcessMetaData processMetaData)
{
return (withInferredInputFieldsExcluding(processMetaData, Collections.emptySet()));
}
/*******************************************************************************
** find all input fields in frontend steps of the process, and add them as fields
** in this container, unless they're in the collection to exclude.
*******************************************************************************/
public ApiProcessInputFieldsContainer withInferredInputFieldsExcluding(QProcessMetaData processMetaData, Collection<String> minusFieldNames)
{
if(fields == null)
{
fields = new ArrayList<>();
}
for(QStepMetaData stepMetaData : CollectionUtils.nonNullList(processMetaData.getStepList()))
{
if(stepMetaData instanceof QFrontendStepMetaData frontendStep)
{
for(QFieldMetaData inputField : frontendStep.getInputFields())
{
if(minusFieldNames != null && !minusFieldNames.contains(inputField.getName()))
{
fields.add(inputField);
}
}
}
}
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

@ -0,0 +1,612 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.api.model.metadata.processes;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.api.ApiSupplementType;
import com.kingsrook.qqq.api.model.APIVersionRange;
import com.kingsrook.qqq.api.model.metadata.fields.ApiFieldMetaData;
import com.kingsrook.qqq.api.model.metadata.fields.ApiFieldMetaDataContainer;
import com.kingsrook.qqq.api.model.openapi.HttpMethod;
import com.kingsrook.qqq.backend.core.instances.QInstanceEnricher;
import com.kingsrook.qqq.backend.core.instances.QInstanceValidator;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
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;
import org.apache.commons.lang.BooleanUtils;
/*******************************************************************************
**
*******************************************************************************/
public class ApiProcessMetaData
{
private String initialVersion;
private String finalVersion;
private String apiProcessName;
private Boolean isExcluded;
private Boolean overrideProcessIsHidden;
private String path;
private HttpMethod method;
private String summary;
private String description;
private AsyncMode asyncMode = AsyncMode.OPTIONAL;
private ApiProcessInput input;
private ApiProcessOutputInterface output;
private Map<String, QCodeReference> customizers;
public enum AsyncMode
{
NEVER,
OPTIONAL,
ALWAYS
}
/*******************************************************************************
**
*******************************************************************************/
public APIVersionRange getApiVersionRange()
{
if(getInitialVersion() == null)
{
return APIVersionRange.none();
}
return (getFinalVersion() != null
? APIVersionRange.betweenAndIncluding(getInitialVersion(), getFinalVersion())
: APIVersionRange.afterAndIncluding(getInitialVersion()));
}
/*******************************************************************************
**
*******************************************************************************/
public void enrich(QInstanceEnricher qInstanceEnricher, String apiName, QProcessMetaData process)
{
if(!StringUtils.hasContent(getApiProcessName()))
{
setApiProcessName(process.getName());
}
if(initialVersion != null)
{
if(getOutput() instanceof ApiProcessObjectOutput outputObject)
{
enrichFieldList(qInstanceEnricher, apiName, outputObject.getOutputFields());
}
if(input != null)
{
for(ApiProcessInputFieldsContainer fieldsContainer : ListBuilder.of(input.getQueryStringParams(), input.getFormParams(), input.getObjectBodyParams()))
{
if(fieldsContainer != null)
{
enrichFieldList(qInstanceEnricher, apiName, fieldsContainer.getFields());
}
}
if(input.getBodyField() != null)
{
enrichFieldList(qInstanceEnricher, apiName, List.of(input.getBodyField()));
}
}
}
}
/*******************************************************************************
**
*******************************************************************************/
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);
}
}
/*******************************************************************************
**
*******************************************************************************/
private static ApiFieldMetaData ensureFieldHasApiSupplementalMetaData(String apiName, QFieldMetaData field)
{
if(field.getSupplementalMetaData(ApiSupplementType.NAME) == null)
{
field.withSupplementalMetaData(new ApiFieldMetaDataContainer());
}
ApiFieldMetaDataContainer apiFieldMetaDataContainer = ApiFieldMetaDataContainer.of(field);
if(apiFieldMetaDataContainer.getApiFieldMetaData(apiName) == null)
{
apiFieldMetaDataContainer.withApiFieldMetaData(apiName, new ApiFieldMetaData());
}
return (apiFieldMetaDataContainer.getApiFieldMetaData(apiName));
}
/*******************************************************************************
** Getter for initialVersion
*******************************************************************************/
public String getInitialVersion()
{
return (this.initialVersion);
}
/*******************************************************************************
** Setter for initialVersion
*******************************************************************************/
public void setInitialVersion(String initialVersion)
{
this.initialVersion = initialVersion;
}
/*******************************************************************************
** Fluent setter for initialVersion
*******************************************************************************/
public ApiProcessMetaData withInitialVersion(String initialVersion)
{
this.initialVersion = initialVersion;
return (this);
}
/*******************************************************************************
** Getter for finalVersion
*******************************************************************************/
public String getFinalVersion()
{
return (this.finalVersion);
}
/*******************************************************************************
** Setter for finalVersion
*******************************************************************************/
public void setFinalVersion(String finalVersion)
{
this.finalVersion = finalVersion;
}
/*******************************************************************************
** Fluent setter for finalVersion
*******************************************************************************/
public ApiProcessMetaData withFinalVersion(String finalVersion)
{
this.finalVersion = finalVersion;
return (this);
}
/*******************************************************************************
** Getter for apiProcessName
*******************************************************************************/
public String getApiProcessName()
{
return (this.apiProcessName);
}
/*******************************************************************************
** Setter for apiProcessName
*******************************************************************************/
public void setApiProcessName(String apiProcessName)
{
this.apiProcessName = apiProcessName;
}
/*******************************************************************************
** Fluent setter for apiProcessName
*******************************************************************************/
public ApiProcessMetaData withApiProcessName(String apiProcessName)
{
this.apiProcessName = apiProcessName;
return (this);
}
/*******************************************************************************
** Getter for isExcluded
*******************************************************************************/
public Boolean getIsExcluded()
{
return (this.isExcluded);
}
/*******************************************************************************
** Setter for isExcluded
*******************************************************************************/
public void setIsExcluded(Boolean isExcluded)
{
this.isExcluded = isExcluded;
}
/*******************************************************************************
** Fluent setter for isExcluded
*******************************************************************************/
public ApiProcessMetaData withIsExcluded(Boolean isExcluded)
{
this.isExcluded = isExcluded;
return (this);
}
/*******************************************************************************
** Getter for method
*******************************************************************************/
public HttpMethod getMethod()
{
return (this.method);
}
/*******************************************************************************
** Setter for method
*******************************************************************************/
public void setMethod(HttpMethod method)
{
this.method = method;
}
/*******************************************************************************
** Fluent setter for method
*******************************************************************************/
public ApiProcessMetaData withMethod(HttpMethod method)
{
this.method = method;
return (this);
}
/*******************************************************************************
** Getter for path
*******************************************************************************/
public String getPath()
{
return (this.path);
}
/*******************************************************************************
** Setter for path
*******************************************************************************/
public void setPath(String path)
{
this.path = path;
}
/*******************************************************************************
** Fluent setter for path
*******************************************************************************/
public ApiProcessMetaData withPath(String path)
{
this.path = path;
return (this);
}
/*******************************************************************************
** Getter for customizers
*******************************************************************************/
public Map<String, QCodeReference> getCustomizers()
{
return (this.customizers);
}
/*******************************************************************************
** Setter for customizers
*******************************************************************************/
public void setCustomizers(Map<String, QCodeReference> customizers)
{
this.customizers = customizers;
}
/*******************************************************************************
** Fluent setter for customizers
*******************************************************************************/
public ApiProcessMetaData withCustomizers(Map<String, QCodeReference> customizers)
{
this.customizers = customizers;
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
public ApiProcessMetaData withCustomizer(String role, QCodeReference customizer)
{
if(this.customizers == null)
{
this.customizers = new HashMap<>();
}
if(this.customizers.containsKey(role))
{
throw (new IllegalArgumentException("Attempt to add a second customizer with role [" + role + "] to apiProcess [" + apiProcessName + "]."));
}
this.customizers.put(role, customizer);
return (this);
}
/*******************************************************************************
** 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);
}
/*******************************************************************************
** Getter for summary
*******************************************************************************/
public String getSummary()
{
return (this.summary);
}
/*******************************************************************************
** Setter for summary
*******************************************************************************/
public void setSummary(String summary)
{
this.summary = summary;
}
/*******************************************************************************
** Fluent setter for summary
*******************************************************************************/
public ApiProcessMetaData withSummary(String summary)
{
this.summary = summary;
return (this);
}
/*******************************************************************************
** Getter for description
*******************************************************************************/
public String getDescription()
{
return (this.description);
}
/*******************************************************************************
** Setter for description
*******************************************************************************/
public void setDescription(String description)
{
this.description = description;
}
/*******************************************************************************
** Fluent setter for description
*******************************************************************************/
public ApiProcessMetaData withDescription(String description)
{
this.description = description;
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
public void validate(QInstance qInstance, QProcessMetaData process, QInstanceValidator qInstanceValidator, String apiName)
{
if(BooleanUtils.isTrue(getIsExcluded()))
{
/////////////////////////////////////////////////
// no validation needed for excluded processes //
/////////////////////////////////////////////////
return;
}
qInstanceValidator.assertCondition(getMethod() != null, "Missing a method for api process meta data for process: " + process.getName() + ", apiName: " + apiName);
}
/*******************************************************************************
** Getter for asyncMode
*******************************************************************************/
public AsyncMode getAsyncMode()
{
return (this.asyncMode);
}
/*******************************************************************************
** Setter for asyncMode
*******************************************************************************/
public void setAsyncMode(AsyncMode asyncMode)
{
this.asyncMode = asyncMode;
}
/*******************************************************************************
** Fluent setter for asyncMode
*******************************************************************************/
public ApiProcessMetaData withAsyncMode(AsyncMode asyncMode)
{
this.asyncMode = asyncMode;
return (this);
}
/*******************************************************************************
** Getter for overrideProcessIsHidden
*******************************************************************************/
public Boolean getOverrideProcessIsHidden()
{
return (this.overrideProcessIsHidden);
}
/*******************************************************************************
** Setter for overrideProcessIsHidden
*******************************************************************************/
public void setOverrideProcessIsHidden(Boolean overrideProcessIsHidden)
{
this.overrideProcessIsHidden = overrideProcessIsHidden;
}
/*******************************************************************************
** Fluent setter for overrideProcessIsHidden
*******************************************************************************/
public ApiProcessMetaData withOverrideProcessIsHidden(Boolean overrideProcessIsHidden)
{
this.overrideProcessIsHidden = overrideProcessIsHidden;
return (this);
}
}

View File

@ -0,0 +1,189 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.api.model.metadata.processes;
import java.util.LinkedHashMap;
import java.util.Map;
import com.kingsrook.qqq.api.ApiSupplementType;
import com.kingsrook.qqq.backend.core.instances.QInstanceEnricher;
import com.kingsrook.qqq.backend.core.instances.QInstanceValidator;
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.processes.QSupplementalProcessMetaData;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
/*******************************************************************************
**
*******************************************************************************/
public class ApiProcessMetaDataContainer extends QSupplementalProcessMetaData
{
private Map<String, ApiProcessMetaData> apis;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public ApiProcessMetaDataContainer()
{
setType(ApiSupplementType.NAME);
}
/*******************************************************************************
**
*******************************************************************************/
public static ApiProcessMetaDataContainer of(QProcessMetaData process)
{
return ((ApiProcessMetaDataContainer) process.getSupplementalMetaData(ApiSupplementType.NAME));
}
/*******************************************************************************
** either get the container attached to a field - or create a new one and attach
** it to the field, and return that.
*******************************************************************************/
public static ApiProcessMetaDataContainer ofOrWithNew(QProcessMetaData process)
{
ApiProcessMetaDataContainer apiProcessMetaDataContainer = (ApiProcessMetaDataContainer) process.getSupplementalMetaData(ApiSupplementType.NAME);
if(apiProcessMetaDataContainer == null)
{
apiProcessMetaDataContainer = new ApiProcessMetaDataContainer();
process.withSupplementalMetaData(apiProcessMetaDataContainer);
}
return (apiProcessMetaDataContainer);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void enrich(QInstanceEnricher qInstanceEnricher, QProcessMetaData process)
{
super.enrich(qInstanceEnricher, process);
for(Map.Entry<String, ApiProcessMetaData> entry : CollectionUtils.nonNullMap(apis).entrySet())
{
entry.getValue().enrich(qInstanceEnricher, entry.getKey(), process);
}
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void validate(QInstance qInstance, QProcessMetaData process, QInstanceValidator qInstanceValidator)
{
super.validate(qInstance, process, qInstanceValidator);
for(Map.Entry<String, ApiProcessMetaData> entry : CollectionUtils.nonNullMap(apis).entrySet())
{
entry.getValue().validate(qInstance, process, qInstanceValidator, entry.getKey());
}
}
/*******************************************************************************
** Getter for apis
*******************************************************************************/
public Map<String, ApiProcessMetaData> getApis()
{
return (this.apis);
}
/*******************************************************************************
** Getter for apis
*******************************************************************************/
public ApiProcessMetaData getApiProcessMetaData(String apiName)
{
if(this.apis == null)
{
return (null);
}
return (this.apis.get(apiName));
}
/*******************************************************************************
**
*******************************************************************************/
public ApiProcessMetaData getApiProcessMetaDataOrWithNew(String apiName)
{
ApiProcessMetaData apiProcessMetaData = getApiProcessMetaData(apiName);
if(apiProcessMetaData == null)
{
apiProcessMetaData = new ApiProcessMetaData();
withApiProcessMetaData(apiName, apiProcessMetaData);
}
return (apiProcessMetaData);
}
/*******************************************************************************
** Setter for apis
*******************************************************************************/
public void setApis(Map<String, ApiProcessMetaData> apis)
{
this.apis = apis;
}
/*******************************************************************************
** Fluent setter for apis
*******************************************************************************/
public ApiProcessMetaDataContainer withApis(Map<String, ApiProcessMetaData> apis)
{
this.apis = apis;
return (this);
}
/*******************************************************************************
** Fluent setter for apis
*******************************************************************************/
public ApiProcessMetaDataContainer withApiProcessMetaData(String apiName, ApiProcessMetaData apiProcessMetaData)
{
if(this.apis == null)
{
this.apis = new LinkedHashMap<>();
}
this.apis.put(apiName, apiProcessMetaData);
return (this);
}
}

View File

@ -0,0 +1,243 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.api.model.metadata.processes;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import com.kingsrook.qqq.api.actions.GenerateOpenApiSpecAction;
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.Content;
import com.kingsrook.qqq.api.model.openapi.ExampleWithListValue;
import com.kingsrook.qqq.api.model.openapi.ExampleWithSingleValue;
import com.kingsrook.qqq.api.model.openapi.Response;
import com.kingsrook.qqq.api.model.openapi.Schema;
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;
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 io.javalin.http.ContentType;
import org.eclipse.jetty.http.HttpStatus;
/*******************************************************************************
**
*******************************************************************************/
public class ApiProcessObjectOutput implements ApiProcessOutputInterface
{
private List<QFieldMetaData> outputFields;
private String responseDescription;
private HttpStatus.Code successResponseCode;
/*******************************************************************************
**
*******************************************************************************/
@Override
public HttpStatus.Code getSuccessStatusCode(RunProcessInput runProcessInput, RunProcessOutput runProcessOutput)
{
return (HttpStatus.Code.OK);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public Map<Integer, Response> getSpecResponses(String apiName)
{
Map<String, Schema> properties = new LinkedHashMap<>();
for(QFieldMetaData outputField : CollectionUtils.nonNullList(outputFields))
{
ApiFieldMetaDataContainer apiFieldMetaDataContainer = ApiFieldMetaDataContainer.ofOrNew(outputField);
ApiFieldMetaData apiFieldMetaData = apiFieldMetaDataContainer.getApiFieldMetaData(apiName);
Object example = null;
if(apiFieldMetaData != null)
{
if(apiFieldMetaData.getExample() instanceof ExampleWithSingleValue exampleWithSingleValue)
{
example = exampleWithSingleValue.getValue();
}
else if(apiFieldMetaData.getExample() instanceof ExampleWithListValue exampleWithListValue)
{
example = exampleWithListValue.getValue();
}
}
properties.put(outputField.getName(), new Schema()
.withDescription(apiFieldMetaData == null ? null : apiFieldMetaData.getDescription())
.withExample(example)
.withNullable(!outputField.getIsRequired())
.withType(GenerateOpenApiSpecAction.getFieldType(outputField))
);
}
return (MapBuilder.of(
Objects.requireNonNullElse(successResponseCode, HttpStatus.Code.OK).getCode(),
new Response()
.withDescription(ObjectUtils.requireConditionElse(responseDescription, StringUtils::hasContent, "Process has been successfully executed."))
.withContent(MapBuilder.of(ContentType.JSON, new Content()
.withSchema(new Schema()
.withType("object")
.withProperties(properties))))
));
}
/*******************************************************************************
**
******************************************************************************/
@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);
}
/*******************************************************************************
** Getter for responseDescription
*******************************************************************************/
public String getResponseDescription()
{
return (this.responseDescription);
}
/*******************************************************************************
** Setter for responseDescription
*******************************************************************************/
public void setResponseDescription(String responseDescription)
{
this.responseDescription = responseDescription;
}
/*******************************************************************************
** Fluent setter for responseDescription
*******************************************************************************/
public ApiProcessObjectOutput withResponseDescription(String responseDescription)
{
this.responseDescription = responseDescription;
return (this);
}
/*******************************************************************************
** Getter for successResponseCode
*******************************************************************************/
public HttpStatus.Code getSuccessResponseCode()
{
return (this.successResponseCode);
}
/*******************************************************************************
** Setter for successResponseCode
*******************************************************************************/
public void setSuccessResponseCode(HttpStatus.Code successResponseCode)
{
this.successResponseCode = successResponseCode;
}
/*******************************************************************************
** Fluent setter for successResponseCode
*******************************************************************************/
public ApiProcessObjectOutput withSuccessResponseCode(HttpStatus.Code successResponseCode)
{
this.successResponseCode = successResponseCode;
return (this);
}
}

View File

@ -0,0 +1,64 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.api.model.metadata.processes;
import java.io.Serializable;
import java.util.Map;
import com.kingsrook.qqq.api.model.openapi.Response;
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 com.kingsrook.qqq.backend.core.utils.collections.MapBuilder;
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.NO_CONTENT);
}
/*******************************************************************************
**
*******************************************************************************/
default Map<Integer, Response> getSpecResponses(String apiName)
{
return (MapBuilder.of(
HttpStatus.Code.NO_CONTENT.getCode(), new Response()
.withDescription("Process has been successfully executed.")
));
}
}

View File

@ -0,0 +1,266 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.api.model.metadata.processes;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.api.model.openapi.Content;
import com.kingsrook.qqq.api.model.openapi.Response;
import com.kingsrook.qqq.api.model.openapi.Schema;
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 com.kingsrook.qqq.backend.core.utils.collections.ListBuilder;
import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder;
import io.javalin.http.ContentType;
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 Map<Integer, Response> getSpecResponses(String apiName)
{
Map<String, Schema> propertiesFor207Object = new LinkedHashMap<>();
propertiesFor207Object.put("id", new Schema().withType("integer").withDescription("Id of the record whose status is being described in the object"));
propertiesFor207Object.put("statusCode", new Schema().withType("integer").withDescription("HTTP Status code indicating the success or failure of the process on this record"));
propertiesFor207Object.put("statusText", new Schema().withType("string").withDescription("HTTP Status text indicating the success or failure of the process on this record"));
propertiesFor207Object.put("message", new Schema().withType("string").withDescription("Additional descriptive information about the result of the process on this record."));
List<Object> exampleFor207Object = ListBuilder.of(MapBuilder.of(LinkedHashMap::new)
.with("id", 42)
.with("statusCode", io.javalin.http.HttpStatus.OK.getCode())
.with("statusText", io.javalin.http.HttpStatus.OK.getMessage())
.with("message", "record was processed successfully.")
.build(),
MapBuilder.of(LinkedHashMap::new)
.with("id", 47)
.with("statusCode", io.javalin.http.HttpStatus.BAD_REQUEST.getCode())
.with("statusText", io.javalin.http.HttpStatus.BAD_REQUEST.getMessage())
.with("message", "error executing process on record.")
.build());
return MapBuilder.of(
HttpStatus.Code.MULTI_STATUS.getCode(), new Response()
.withDescription("For each input record, an object describing its status may be returned.")
.withContent(MapBuilder.of(ContentType.JSON, new Content()
.withSchema(new Schema()
.withType("array")
.withItems(new Schema()
.withType("object")
.withProperties(propertiesFor207Object))
.withExample(exampleFor207Object)
)
)),
HttpStatus.Code.NO_CONTENT.getCode(), new Response()
.withDescription("If no records were found, there may be no content in the response.")
);
}
/*******************************************************************************
**
*******************************************************************************/
@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.pickMessage(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)
{
apiOutput.add(toMap(processSummaryRecordLink));
}
else if(processSummaryLineInterface instanceof ProcessSummaryFilterLink processSummaryFilterLink)
{
apiOutput.add(toMap(processSummaryFilterLink));
}
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));
}
}
/*******************************************************************************
**
*******************************************************************************/
private HashMap<String, Serializable> toMap(ProcessSummaryFilterLink processSummaryFilterLink)
{
HashMap<String, Serializable> map = initResultMapForProcessSummaryLine(processSummaryFilterLink);
String messagePrefix = getResultMapMessagePrefix(processSummaryFilterLink);
map.put("message", messagePrefix + processSummaryFilterLink.getFullText());
return (map);
}
/*******************************************************************************
**
*******************************************************************************/
private HashMap<String, Serializable> toMap(ProcessSummaryRecordLink processSummaryRecordLink)
{
HashMap<String, Serializable> map = initResultMapForProcessSummaryLine(processSummaryRecordLink);
String messagePrefix = getResultMapMessagePrefix(processSummaryRecordLink);
map.put("message", messagePrefix + processSummaryRecordLink.getFullText());
return (map);
}
/*******************************************************************************
**
*******************************************************************************/
private static HashMap<String, Serializable> toMap(ProcessSummaryLine processSummaryLine)
{
HashMap<String, Serializable> map = initResultMapForProcessSummaryLine(processSummaryLine);
String messagePrefix = getResultMapMessagePrefix(processSummaryLine);
map.put("message", messagePrefix + processSummaryLine.getMessage());
return (map);
}
/*******************************************************************************
**
*******************************************************************************/
private static String getResultMapMessagePrefix(ProcessSummaryLineInterface processSummaryLine)
{
@SuppressWarnings("checkstyle:indentation")
String messagePrefix = switch(processSummaryLine.getStatus())
{
case OK, INFO, ERROR -> "";
case WARNING -> "Warning: ";
};
return messagePrefix;
}
/*******************************************************************************
**
*******************************************************************************/
private static HashMap<String, Serializable> initResultMapForProcessSummaryLine(ProcessSummaryLineInterface processSummaryLine)
{
HashMap<String, Serializable> map = new HashMap<>();
@SuppressWarnings("checkstyle:indentation")
HttpStatus.Code code = switch(processSummaryLine.getStatus())
{
case OK, WARNING, INFO -> HttpStatus.Code.OK;
case ERROR -> HttpStatus.Code.INTERNAL_SERVER_ERROR;
};
map.put("statusCode", code.getCode());
map.put("statusText", code.getMessage());
return map;
}
}

View File

@ -0,0 +1,193 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.api.model.metadata.processes;
import java.util.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."));
}
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."));
}
if(BooleanUtils.isTrue(process.getIsHidden()))
{
if(!BooleanUtils.isTrue(apiProcessMetaData.getOverrideProcessIsHidden()))
{
LOG.info("404 because process isHidden", 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

@ -0,0 +1,41 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.api.model.metadata.processes;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput;
/*******************************************************************************
**
*******************************************************************************/
public interface PostRunApiProcessCustomizer
{
/*******************************************************************************
**
*******************************************************************************/
void postApiRun(RunProcessInput runProcessInput, RunProcessOutput runProcessOutput) throws QException;
}

View File

@ -0,0 +1,40 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.api.model.metadata.processes;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput;
/*******************************************************************************
**
*******************************************************************************/
public interface PreRunApiProcessCustomizer
{
/*******************************************************************************
**
*******************************************************************************/
void preApiRun(RunProcessInput runProcessInput) throws QException;
}

View File

@ -27,7 +27,7 @@ import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import com.kingsrook.qqq.api.ApiMiddlewareType;
import com.kingsrook.qqq.api.ApiSupplementType;
import com.kingsrook.qqq.api.model.APIVersionRange;
import com.kingsrook.qqq.api.model.metadata.ApiOperation;
import com.kingsrook.qqq.api.model.metadata.fields.ApiFieldMetaData;
@ -81,7 +81,7 @@ public class ApiTableMetaData implements ApiOperation.EnabledOperationsProvider
{
for(QFieldMetaData field : table.getFields().values())
{
ApiFieldMetaData apiFieldMetaData = ensureFieldHasApiMiddlewareMetaData(apiName, field);
ApiFieldMetaData apiFieldMetaData = ensureFieldHasApiSupplementalMetaData(apiName, field);
if(apiFieldMetaData.getInitialVersion() == null)
{
apiFieldMetaData.setInitialVersion(initialVersion);
@ -90,7 +90,7 @@ public class ApiTableMetaData implements ApiOperation.EnabledOperationsProvider
for(QFieldMetaData field : CollectionUtils.nonNullList(removedApiFields))
{
ApiFieldMetaData apiFieldMetaData = ensureFieldHasApiMiddlewareMetaData(apiName, field);
ApiFieldMetaData apiFieldMetaData = ensureFieldHasApiSupplementalMetaData(apiName, field);
if(apiFieldMetaData.getInitialVersion() == null)
{
apiFieldMetaData.setInitialVersion(initialVersion);
@ -104,11 +104,11 @@ public class ApiTableMetaData implements ApiOperation.EnabledOperationsProvider
/*******************************************************************************
**
*******************************************************************************/
private static ApiFieldMetaData ensureFieldHasApiMiddlewareMetaData(String apiName, QFieldMetaData field)
private static ApiFieldMetaData ensureFieldHasApiSupplementalMetaData(String apiName, QFieldMetaData field)
{
if(field.getMiddlewareMetaData(ApiMiddlewareType.NAME) == null)
if(field.getSupplementalMetaData(ApiSupplementType.NAME) == null)
{
field.withMiddlewareMetaData(new ApiFieldMetaDataContainer());
field.withSupplementalMetaData(new ApiFieldMetaDataContainer());
}
ApiFieldMetaDataContainer apiFieldMetaDataContainer = ApiFieldMetaDataContainer.of(field);

View File

@ -24,8 +24,8 @@ package com.kingsrook.qqq.api.model.metadata.tables;
import java.util.LinkedHashMap;
import java.util.Map;
import com.kingsrook.qqq.api.ApiMiddlewareType;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QMiddlewareTableMetaData;
import com.kingsrook.qqq.api.ApiSupplementType;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QSupplementalTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
@ -33,7 +33,7 @@ import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
/*******************************************************************************
**
*******************************************************************************/
public class ApiTableMetaDataContainer extends QMiddlewareTableMetaData
public class ApiTableMetaDataContainer extends QSupplementalTableMetaData
{
private Map<String, ApiTableMetaData> apis;
@ -55,7 +55,7 @@ public class ApiTableMetaDataContainer extends QMiddlewareTableMetaData
*******************************************************************************/
public static ApiTableMetaDataContainer of(QTableMetaData table)
{
return ((ApiTableMetaDataContainer) table.getMiddlewareMetaData(ApiMiddlewareType.NAME));
return ((ApiTableMetaDataContainer) table.getSupplementalMetaData(ApiSupplementType.NAME));
}

View File

@ -22,12 +22,15 @@
package com.kingsrook.qqq.api.model.openapi;
import java.io.Serializable;
/*******************************************************************************
**
*******************************************************************************/
public class ExampleWithSingleValue extends Example
{
private String value;
private Serializable value;
@ -46,7 +49,7 @@ public class ExampleWithSingleValue extends Example
/*******************************************************************************
** Getter for value
*******************************************************************************/
public String getValue()
public Serializable getValue()
{
return (this.value);
}
@ -56,7 +59,7 @@ public class ExampleWithSingleValue extends Example
/*******************************************************************************
** Setter for value
*******************************************************************************/
public void setValue(String value)
public void setValue(Serializable value)
{
this.value = value;
}
@ -66,7 +69,7 @@ public class ExampleWithSingleValue extends Example
/*******************************************************************************
** Fluent setter for value
*******************************************************************************/
public ExampleWithSingleValue withValue(String value)
public ExampleWithSingleValue withValue(Serializable value)
{
this.value = value;
return (this);

View File

@ -0,0 +1,35 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.api.model.openapi;
/*******************************************************************************
**
*******************************************************************************/
public enum HttpMethod
{
GET,
POST,
PUT,
PATCH,
DELETE
}

View File

@ -37,6 +37,7 @@ public class Parameter
private Schema schema;
private Boolean explode;
private Map<String, Example> examples;
private Example example;
@ -255,4 +256,35 @@ public class Parameter
return (this);
}
/*******************************************************************************
** Getter for example
*******************************************************************************/
public Example getExample()
{
return (this.example);
}
/*******************************************************************************
** Setter for examplee
*******************************************************************************/
public void setExample(Example example)
{
this.example = example;
}
/*******************************************************************************
** Fluent setter for examplee
*******************************************************************************/
public Parameter withExample(Example example)
{
this.example = example;
return (this);
}
}

View File

@ -22,6 +22,7 @@
package com.kingsrook.qqq.api.model.openapi;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonGetter;
@ -191,6 +192,27 @@ public class Schema
/*******************************************************************************
** Setter for example
*******************************************************************************/
public void setExample(BigDecimal example)
{
this.example = example;
}
/*******************************************************************************
** Fluent setter for example
*******************************************************************************/
public Schema withExample(Object example)
{
this.example = example;
return (this);
}
/*******************************************************************************
** Fluent setter for example
*******************************************************************************/

View File

@ -30,6 +30,7 @@ import java.util.Map;
import com.kingsrook.qqq.api.actions.ApiImplementation;
import com.kingsrook.qqq.api.actions.QRecordApiAdapter;
import com.kingsrook.qqq.api.model.APIVersion;
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.backend.core.actions.scripts.QCodeExecutor;
@ -38,6 +39,9 @@ import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import org.json.JSONObject;
/*******************************************************************************
@ -277,6 +281,53 @@ public class ApiScriptUtils implements QCodeExecutorAware, Serializable
/*******************************************************************************
**
*******************************************************************************/
public Serializable runProcess(String processApiName) throws QException
{
return (runProcess(processApiName, null));
}
/*******************************************************************************
**
*******************************************************************************/
public Serializable runProcess(String processApiName, Object params) throws QException
{
validateApiNameAndVersion("runProcess(" + processApiName + ")");
Map<String, String> paramMap = new LinkedHashMap<>();
String paramsString = ValueUtils.getValueAsString(params);
if(StringUtils.hasContent(paramsString))
{
JSONObject paramsJSON = new JSONObject(paramsString);
for(String fieldName : paramsJSON.keySet())
{
paramMap.put(fieldName, paramsJSON.optString(fieldName));
}
}
HttpApiResponse httpApiResponse = ApiImplementation.runProcess(getApiInstanceMetaData(), apiVersion, processApiName, paramMap);
return (httpApiResponse.getResponseBodyObject());
}
/*******************************************************************************
**
*******************************************************************************/
public Serializable getProcessStatus(String processApiName, String jobId) throws QException
{
validateApiNameAndVersion("getProcessStatus(" + processApiName + ")");
HttpApiResponse httpApiResponse = ApiImplementation.getProcessStatus(getApiInstanceMetaData(), apiVersion, processApiName, jobId);
return (httpApiResponse.getResponseBodyObject());
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -38,7 +38,7 @@
show-header="false"
allow-spec-file-download="true"
primary-color="{primaryColor}"
sort-endpoints-by="method"
sort-endpoints-by="none"
allow-authentication="true"
persist-auth="true"
render-style="focused"

View File

@ -0,0 +1,48 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.api;
import java.math.BigDecimal;
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
/*******************************************************************************
**
*******************************************************************************/
public class GetPersonInfoStep implements BackendStep
{
/*******************************************************************************
**
*******************************************************************************/
@Override
public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
{
runBackendStepOutput.addValue("density", new BigDecimal("3.50"));
runBackendStepOutput.addValue("daysOld", runBackendStepInput.getValueInteger("age") * 365);
runBackendStepOutput.addValue("nickname", "Guy from " + runBackendStepInput.getValueString("homeTown"));
}
}

View File

@ -29,8 +29,15 @@ 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;
import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPreDeleteCustomizer;
import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPreInsertCustomizer;
import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPreUpdateCustomizer;
@ -45,12 +52,23 @@ import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.authentication.Auth0AuthenticationMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.nocode.HtmlWrapper;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.nocode.WidgetHtmlLine;
import com.kingsrook.qqq.backend.core.model.metadata.fields.DisplayFormat;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn;
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinType;
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.PVSValueFormatAndFields;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType;
import com.kingsrook.qqq.backend.core.model.metadata.processes.NoCodeWidgetFrontendComponentMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QComponentType;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendComponentMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Association;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
@ -58,6 +76,9 @@ 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;
@ -74,6 +95,9 @@ 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_TRANSFORM_PEOPLE = "transformPeople";
public static final String API_NAME = "test-api";
public static final String ALTERNATIVE_API_NAME = "person-api";
@ -103,9 +127,13 @@ public class TestUtils
qInstance.addJoin(defineJoinLineItemLineItemExtrinsic());
qInstance.addJoin(defineJoinOrderOrderExtrinsic());
qInstance.addPossibleValueSource(definePersonPossibleValueSource());
qInstance.addProcess(defineProcessGetPersonInfo());
qInstance.addProcess(defineProcessTransformPeople());
qInstance.setAuthentication(new Auth0AuthenticationMetaData().withType(QAuthenticationType.FULLY_ANONYMOUS).withName("anonymous"));
qInstance.withMiddlewareMetaData(new ApiInstanceMetaDataContainer()
qInstance.withSupplementalMetaData(new ApiInstanceMetaDataContainer()
.withApiInstanceMetaData(new ApiInstanceMetaData()
.withName(API_NAME)
.withPath("/api/")
@ -133,6 +161,109 @@ public class TestUtils
/*******************************************************************************
**
*******************************************************************************/
private static QPossibleValueSource definePersonPossibleValueSource()
{
return new QPossibleValueSource()
.withName(TABLE_NAME_PERSON)
.withType(QPossibleValueSourceType.TABLE)
.withTableName(TABLE_NAME_PERSON)
.withValueFormatAndFields(PVSValueFormatAndFields.LABEL_ONLY);
}
/*******************************************************************************
**
*******************************************************************************/
private static QProcessMetaData defineProcessGetPersonInfo()
{
QProcessMetaData process = new QProcessMetaData()
.withName(PROCESS_NAME_GET_PERSON_INFO)
.withLabel("Get Person Info")
.withTableName(TABLE_NAME_PERSON)
.addStep(new QFrontendStepMetaData()
.withName("enterInputs")
.withLabel("Person Info Input")
.withComponent(new QFrontendComponentMetaData().withType(QComponentType.EDIT_FORM))
.withFormField(new QFieldMetaData("age", QFieldType.INTEGER).withIsRequired(true))
.withFormField(new QFieldMetaData("partnerPersonId", QFieldType.INTEGER).withPossibleValueSourceName(TABLE_NAME_PERSON))
.withFormField(new QFieldMetaData("heightInches", QFieldType.DECIMAL).withIsRequired(true))
.withFormField(new QFieldMetaData("weightPounds", QFieldType.INTEGER).withIsRequired(true))
.withFormField(new QFieldMetaData("homeTown", QFieldType.STRING).withIsRequired(true))
.withComponent(new NoCodeWidgetFrontendComponentMetaData()
.withOutput(new WidgetHtmlLine()
.withWrapper(HtmlWrapper.divWithStyles(HtmlWrapper.STYLE_FLOAT_RIGHT, HtmlWrapper.STYLE_MEDIUM_CENTERED, HtmlWrapper.styleWidth("50%")))
.withVelocityTemplate("""
<b>Density:</b><br />$density<br/>
"""))
.withOutput(new WidgetHtmlLine()
.withVelocityTemplate("""
<b>Days old:</b> $daysOld<br/>
<b>Nickname:</b> $nickname<br/>
"""))
))
.addStep(new QBackendStepMetaData()
.withName("execute")
.withCode(new QCodeReference(GetPersonInfoStep.class)))
.addStep(new QFrontendStepMetaData()
.withName("dummyStep")
);
process.withSupplementalMetaData(new ApiProcessMetaDataContainer()
.withApiProcessMetaData(API_NAME, new ApiProcessMetaData()
.withInitialVersion(CURRENT_API_VERSION)
.withMethod(HttpMethod.GET)
.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);
}
/*******************************************************************************
**
*******************************************************************************/
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
*******************************************************************************/
@ -204,7 +335,7 @@ public class TestUtils
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// make some changes to this table in the "main" api (but leave it like the backend in the ALTERNATIVE_API_NAME) //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
table.withMiddlewareMetaData(new ApiTableMetaDataContainer()
table.withSupplementalMetaData(new ApiTableMetaDataContainer()
.withApiTableMetaData(API_NAME, new ApiTableMetaData()
.withInitialVersion(V2022_Q4)
@ -212,7 +343,7 @@ public class TestUtils
// in 2022.Q4, this table had a "shoeCount" field. but for the 2023.Q1 version, we renamed it to noOfShoes! //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
.withRemovedApiField(new QFieldMetaData("shoeCount", QFieldType.INTEGER).withDisplayFormat(DisplayFormat.COMMAS)
.withMiddlewareMetaData(new ApiFieldMetaDataContainer().withApiFieldMetaData(API_NAME,
.withSupplementalMetaData(new ApiFieldMetaDataContainer().withApiFieldMetaData(API_NAME,
new ApiFieldMetaData().withFinalVersion(V2022_Q4).withReplacedByFieldName("noOfShoes"))))
)
.withApiTableMetaData(ALTERNATIVE_API_NAME, new ApiTableMetaData().withInitialVersion(V2022_Q4)));
@ -220,18 +351,18 @@ public class TestUtils
/////////////////////////////////////////////////////
// change the name for this field for the main api //
/////////////////////////////////////////////////////
table.getField("birthDate").withMiddlewareMetaData(new ApiFieldMetaDataContainer().withApiFieldMetaData(API_NAME, new ApiFieldMetaData().withApiFieldName("birthDay")));
table.getField("birthDate").withSupplementalMetaData(new ApiFieldMetaDataContainer().withApiFieldMetaData(API_NAME, new ApiFieldMetaData().withApiFieldName("birthDay")));
////////////////////////////////////////////////////////////////////////////////
// See above - we renamed this field (in the backend) for the 2023_Q1 version //
////////////////////////////////////////////////////////////////////////////////
table.getField("noOfShoes").withMiddlewareMetaData(new ApiFieldMetaDataContainer().withApiFieldMetaData(API_NAME, new ApiFieldMetaData().withInitialVersion(V2023_Q1)));
table.getField("noOfShoes").withSupplementalMetaData(new ApiFieldMetaDataContainer().withApiFieldMetaData(API_NAME, new ApiFieldMetaData().withInitialVersion(V2023_Q1)));
/////////////////////////////////////////////////////////////////////////////////////////////////
// 2 new fields - one will appear in a future version of the API, the other is always excluded //
/////////////////////////////////////////////////////////////////////////////////////////////////
table.getField("cost").withMiddlewareMetaData(new ApiFieldMetaDataContainer().withApiFieldMetaData(API_NAME, new ApiFieldMetaData().withInitialVersion(V2023_Q2)));
table.getField("price").withMiddlewareMetaData(new ApiFieldMetaDataContainer().withApiFieldMetaData(API_NAME, new ApiFieldMetaData().withIsExcluded(true)));
table.getField("cost").withSupplementalMetaData(new ApiFieldMetaDataContainer().withApiFieldMetaData(API_NAME, new ApiFieldMetaData().withInitialVersion(V2023_Q2)));
table.getField("price").withSupplementalMetaData(new ApiFieldMetaDataContainer().withApiFieldMetaData(API_NAME, new ApiFieldMetaData().withIsExcluded(true)));
return (table);
}
@ -248,7 +379,7 @@ public class TestUtils
.withCustomizer(TableCustomizers.PRE_INSERT_RECORD.getRole(), new QCodeReference(OrderPreInsertCustomizer.class))
.withCustomizer(TableCustomizers.PRE_UPDATE_RECORD.getRole(), new QCodeReference(OrderPreUpdateCustomizer.class))
.withBackendName(MEMORY_BACKEND_NAME)
.withMiddlewareMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData().withInitialVersion(V2022_Q4)))
.withSupplementalMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData().withInitialVersion(V2022_Q4)))
.withPrimaryKeyField("id")
.withAssociation(new Association().withName("orderLines").withAssociatedTableName(TABLE_NAME_LINE_ITEM).withJoinName("orderLineItem"))
.withAssociation(new Association().withName("extrinsics").withAssociatedTableName(TABLE_NAME_ORDER_EXTRINSIC).withJoinName("orderOrderExtrinsic"))
@ -271,7 +402,7 @@ public class TestUtils
return new QTableMetaData()
.withName(TABLE_NAME_LINE_ITEM)
.withBackendName(MEMORY_BACKEND_NAME)
.withMiddlewareMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData().withInitialVersion(V2022_Q4)))
.withSupplementalMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData().withInitialVersion(V2022_Q4)))
.withPrimaryKeyField("id")
.withAssociation(new Association().withName("extrinsics").withAssociatedTableName(TABLE_NAME_LINE_ITEM_EXTRINSIC).withJoinName("lineItemLineItemExtrinsic"))
.withField(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false))
@ -293,7 +424,7 @@ public class TestUtils
return new QTableMetaData()
.withName(TABLE_NAME_LINE_ITEM_EXTRINSIC)
.withBackendName(MEMORY_BACKEND_NAME)
.withMiddlewareMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData().withInitialVersion(V2022_Q4)))
.withSupplementalMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData().withInitialVersion(V2022_Q4)))
.withPrimaryKeyField("id")
.withField(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false))
.withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME).withIsEditable(false))
@ -313,7 +444,7 @@ public class TestUtils
return new QTableMetaData()
.withName(TABLE_NAME_ORDER_EXTRINSIC)
.withBackendName(MEMORY_BACKEND_NAME)
.withMiddlewareMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData().withInitialVersion(V2022_Q4)))
.withSupplementalMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData().withInitialVersion(V2022_Q4)))
.withPrimaryKeyField("id")
.withField(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false))
.withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME).withIsEditable(false))

View File

@ -0,0 +1,80 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.api;
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

@ -34,6 +34,7 @@ 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.QException;
import com.kingsrook.qqq.backend.core.instances.QInstanceEnricher;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
@ -120,7 +121,7 @@ class GenerateOpenApiSpecActionTest extends BaseTest
.withBackendName(TestUtils.MEMORY_BACKEND_NAME)
.withPrimaryKeyField("id")
.withField(new QFieldMetaData("id", QFieldType.INTEGER))
.withMiddlewareMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData()
.withSupplementalMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData()
.withInitialVersion(TestUtils.V2022_Q4))));
qInstance.addTable(new QTableMetaData()
@ -129,7 +130,7 @@ class GenerateOpenApiSpecActionTest extends BaseTest
.withPrimaryKeyField("id")
.withField(new QFieldMetaData("id", QFieldType.INTEGER))
.withIsHidden(true)
.withMiddlewareMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData()
.withSupplementalMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData()
.withInitialVersion(TestUtils.V2022_Q4))));
qInstance.addTable(new QTableMetaData()
@ -137,7 +138,7 @@ class GenerateOpenApiSpecActionTest extends BaseTest
.withBackendName(TestUtils.MEMORY_BACKEND_NAME)
.withPrimaryKeyField("id")
.withField(new QFieldMetaData("id", QFieldType.INTEGER))
.withMiddlewareMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData()
.withSupplementalMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData()
.withIsExcluded(true))));
qInstance.addTable(new QTableMetaData()
@ -151,7 +152,7 @@ class GenerateOpenApiSpecActionTest extends BaseTest
.withBackendName(TestUtils.MEMORY_BACKEND_NAME)
.withPrimaryKeyField("id")
.withField(new QFieldMetaData("id", QFieldType.INTEGER))
.withMiddlewareMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData()
.withSupplementalMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData()
.withInitialVersion(TestUtils.V2023_Q2))));
qInstance.addTable(new QTableMetaData()
@ -159,7 +160,7 @@ class GenerateOpenApiSpecActionTest extends BaseTest
.withBackendName(TestUtils.MEMORY_BACKEND_NAME)
.withPrimaryKeyField("id")
.withField(new QFieldMetaData("id", QFieldType.INTEGER))
.withMiddlewareMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData()
.withSupplementalMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData()
.withInitialVersion(TestUtils.V2022_Q4)
.withFinalVersion(TestUtils.V2022_Q4))));
@ -169,9 +170,11 @@ class GenerateOpenApiSpecActionTest extends BaseTest
.withPrimaryKeyField("id")
.withField(new QFieldMetaData("id", QFieldType.INTEGER))
.withoutCapabilities(Capability.TABLE_QUERY, Capability.TABLE_GET, Capability.TABLE_INSERT, Capability.TABLE_UPDATE, Capability.TABLE_DELETE)
.withMiddlewareMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData()
.withSupplementalMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData()
.withInitialVersion(TestUtils.V2022_Q4))));
new QInstanceEnricher(qInstance).enrich();
GenerateOpenApiSpecOutput output = new GenerateOpenApiSpecAction().execute(new GenerateOpenApiSpecInput().withVersion(TestUtils.CURRENT_API_VERSION).withApiName(TestUtils.API_NAME));
Set<String> apiPaths = output.getOpenAPI().getPaths().keySet();
assertTrue(apiPaths.stream().anyMatch(s -> s.contains("/supportedTable/")));
@ -198,9 +201,11 @@ class GenerateOpenApiSpecActionTest extends BaseTest
.withBackendName(TestUtils.MEMORY_BACKEND_NAME)
.withPrimaryKeyField("id")
.withField(new QFieldMetaData("id", QFieldType.INTEGER))
.withMiddlewareMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData()
.withSupplementalMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData()
.withApiTableName("externalName")
.withInitialVersion(TestUtils.V2022_Q4))));
new QInstanceEnricher(qInstance).enrich();
GenerateOpenApiSpecOutput output = new GenerateOpenApiSpecAction().execute(new GenerateOpenApiSpecInput().withVersion(TestUtils.CURRENT_API_VERSION).withApiName(TestUtils.API_NAME));
Set<String> apiPaths = output.getOpenAPI().getPaths().keySet();

View File

@ -74,11 +74,11 @@ class GetTableApiFieldsActionTest extends BaseTest
QInstance qInstance = QContext.getQInstance();
qInstance.addTable(new QTableMetaData()
.withName(TABLE_NAME)
.withMiddlewareMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData().withInitialVersion("1")))
.withSupplementalMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData().withInitialVersion("1")))
.withField(new QFieldMetaData("a", STRING)) // inherit versionRange from the table
.withField(new QFieldMetaData("b", STRING).withMiddlewareMetaData(new ApiFieldMetaDataContainer().withApiFieldMetaData(TestUtils.API_NAME, new ApiFieldMetaData().withInitialVersion("1"))))
.withField(new QFieldMetaData("c", STRING).withMiddlewareMetaData(new ApiFieldMetaDataContainer().withApiFieldMetaData(TestUtils.API_NAME, new ApiFieldMetaData().withInitialVersion("2"))))
.withField(new QFieldMetaData("d", STRING).withMiddlewareMetaData(new ApiFieldMetaDataContainer().withApiFieldMetaData(TestUtils.API_NAME, new ApiFieldMetaData().withInitialVersion("3"))))
.withField(new QFieldMetaData("b", STRING).withSupplementalMetaData(new ApiFieldMetaDataContainer().withApiFieldMetaData(TestUtils.API_NAME, new ApiFieldMetaData().withInitialVersion("1"))))
.withField(new QFieldMetaData("c", STRING).withSupplementalMetaData(new ApiFieldMetaDataContainer().withApiFieldMetaData(TestUtils.API_NAME, new ApiFieldMetaData().withInitialVersion("2"))))
.withField(new QFieldMetaData("d", STRING).withSupplementalMetaData(new ApiFieldMetaDataContainer().withApiFieldMetaData(TestUtils.API_NAME, new ApiFieldMetaData().withInitialVersion("3"))))
);
new QInstanceEnricher(qInstance).enrich();
@ -98,13 +98,13 @@ class GetTableApiFieldsActionTest extends BaseTest
QInstance qInstance = QContext.getQInstance();
qInstance.addTable(new QTableMetaData()
.withName(TABLE_NAME)
.withMiddlewareMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData().withInitialVersion("1")
.withRemovedApiField(new QFieldMetaData("c", STRING).withMiddlewareMetaData(new ApiFieldMetaDataContainer().withApiFieldMetaData(TestUtils.API_NAME, new ApiFieldMetaData().withInitialVersion("1").withFinalVersion("2"))))
.withSupplementalMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData().withInitialVersion("1")
.withRemovedApiField(new QFieldMetaData("c", STRING).withSupplementalMetaData(new ApiFieldMetaDataContainer().withApiFieldMetaData(TestUtils.API_NAME, new ApiFieldMetaData().withInitialVersion("1").withFinalVersion("2"))))
))
.withField(new QFieldMetaData("a", STRING)) // inherit versionRange from the table
.withField(new QFieldMetaData("b", STRING).withMiddlewareMetaData(new ApiFieldMetaDataContainer().withApiFieldMetaData(TestUtils.API_NAME, new ApiFieldMetaData().withInitialVersion("1"))))
.withField(new QFieldMetaData("b", STRING).withSupplementalMetaData(new ApiFieldMetaDataContainer().withApiFieldMetaData(TestUtils.API_NAME, new ApiFieldMetaData().withInitialVersion("1"))))
// we used to have "c" here... now it's in the removed list above!
.withField(new QFieldMetaData("d", STRING).withMiddlewareMetaData(new ApiFieldMetaDataContainer().withApiFieldMetaData(TestUtils.API_NAME, new ApiFieldMetaData().withInitialVersion("3"))))
.withField(new QFieldMetaData("d", STRING).withSupplementalMetaData(new ApiFieldMetaDataContainer().withApiFieldMetaData(TestUtils.API_NAME, new ApiFieldMetaData().withInitialVersion("3"))))
);
new QInstanceEnricher(qInstance).enrich();

View File

@ -26,6 +26,7 @@ import java.time.LocalDate;
import java.time.Month;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import com.kingsrook.qqq.api.BaseTest;
import com.kingsrook.qqq.api.TestUtils;
import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaData;
@ -50,6 +51,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.modules.authentication.implementations.FullyAnonymousAuthenticationModule;
import com.kingsrook.qqq.backend.core.utils.SleepUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.javalin.QJavalinImplementation;
import io.javalin.apibuilder.EndpointGroup;
@ -67,8 +69,10 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
/*******************************************************************************
@ -98,7 +102,7 @@ class QJavalinApiHandlerTest extends BaseTest
.withBackendName(TestUtils.MEMORY_BACKEND_NAME)
.withPrimaryKeyField("id")
.withField(new QFieldMetaData("id", QFieldType.INTEGER))
.withMiddlewareMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData()
.withSupplementalMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData()
.withApiTableName("externalName")
.withInitialVersion(TestUtils.V2022_Q4))));
@ -1439,6 +1443,82 @@ class QJavalinApiHandlerTest extends BaseTest
/*******************************************************************************
**
*******************************************************************************/
@Test
void testGetProcessForObject() throws QException
{
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").asString();
assertErrorResponse(HttpStatus.BAD_REQUEST_400, "Request failed with 4 reasons: Missing value for required input field", 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());
}
/*******************************************************************************
**
*******************************************************************************/
@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());
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testAsyncProcessAndGetStatus() throws QException
{
insertSimpsons();
HttpResponse<String> response = Unirest.post(BASE_URL + "/api/" + VERSION + "/person/transformPeople?id=1,2,3&async=true").asString();
assertEquals(HttpStatus.ACCEPTED_202, response.getStatus());
JSONObject acceptedJSON = new JSONObject(response.getBody());
String jobId = acceptedJSON.getString("jobId");
assertNotNull(jobId);
for(int i = 0; i < 10; i++)
{
SleepUtils.sleep(100, TimeUnit.MILLISECONDS);
response = Unirest.get(BASE_URL + "/api/" + VERSION + "/person/transformPeople/status/" + jobId).asString();
if(response.getStatus() == HttpStatus.MULTI_STATUS_207)
{
JSONArray jsonArray = new JSONArray(response.getBody());
assertEquals(3, jsonArray.length());
return;
}
}
fail("Never got back a 207, after many sleeps");
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -114,11 +114,11 @@ class ApiInstanceMetaDataTest
qInstance.addTable(new QTableMetaData()
.withName("myValidTable")
.withMiddlewareMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData().withInitialVersion("2023.Q1"))));
.withSupplementalMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData().withInitialVersion("2023.Q1"))));
qInstance.addTable(new QTableMetaData()
.withName("myInvalidTable")
.withMiddlewareMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData().withInitialVersion("notAVersion"))));
.withSupplementalMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData().withInitialVersion("notAVersion"))));
assertValidationErrors(qInstance, makeBaselineValidApiInstanceMetaData()
.withCurrentVersion(new APIVersion("2023.Q1"))
@ -127,7 +127,7 @@ class ApiInstanceMetaDataTest
qInstance.addTable(new QTableMetaData()
.withName("myFutureValidTable")
.withMiddlewareMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData().withInitialVersion("2024.Q1"))));
.withSupplementalMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData().withInitialVersion("2024.Q1"))));
assertValidationErrors(qInstance, makeBaselineValidApiInstanceMetaData()
.withCurrentVersion(new APIVersion("2023.Q1"))
@ -195,7 +195,7 @@ class ApiInstanceMetaDataTest
*******************************************************************************/
private void assertValidationErrors(QInstance qInstance, ApiInstanceMetaData apiInstanceMetaData, List<String> expectedErrors)
{
qInstance.withMiddlewareMetaData(new ApiInstanceMetaDataContainer().withApiInstanceMetaData(apiInstanceMetaData));
qInstance.withSupplementalMetaData(new ApiInstanceMetaDataContainer().withApiInstanceMetaData(apiInstanceMetaData));
QInstanceValidator validator = new QInstanceValidator();
apiInstanceMetaData.validate(apiInstanceMetaData.getName(), qInstance, validator);

View File

@ -25,6 +25,7 @@ package com.kingsrook.qqq.api.utils;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import com.kingsrook.qqq.api.BaseTest;
import com.kingsrook.qqq.api.TestUtils;
import com.kingsrook.qqq.api.javalin.QBadRequestException;
@ -33,9 +34,12 @@ import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QNotFoundException;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
import com.kingsrook.qqq.backend.core.utils.SleepUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import org.junit.jupiter.api.Test;
import static com.kingsrook.qqq.api.TestUtils.insertSimpsons;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@ -262,6 +266,87 @@ class ApiScriptUtilsTest extends BaseTest
/*******************************************************************************
**
*******************************************************************************/
@Test
void testGetProcessForObject() throws QException
{
ApiScriptUtils apiScriptUtils = newDefaultApiScriptUtils();
assertThatThrownBy(() -> apiScriptUtils.runProcess(TestUtils.PROCESS_NAME_GET_PERSON_INFO))
.isInstanceOf(QBadRequestException.class)
.hasMessageContaining("Request failed with 4 reasons: Missing value for required input field");
Object result = apiScriptUtils.runProcess(TestUtils.PROCESS_NAME_GET_PERSON_INFO, """
{"age": 43, "partnerPersonId": 1, "heightInches": 72, "weightPounds": 220, "homeTown": "Chester"}
""");
assertThat(result).isInstanceOf(Map.class);
Map<?, ?> resultMap = (Map<?, ?>) result;
assertEquals(15695, resultMap.get("daysOld"));
assertEquals("Guy from Chester", resultMap.get("nickname"));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testPostProcessForProcessSummaryList() throws QException
{
insertSimpsons();
ApiScriptUtils apiScriptUtils = newDefaultApiScriptUtils();
assertThatThrownBy(() -> apiScriptUtils.runProcess(TestUtils.PROCESS_NAME_TRANSFORM_PEOPLE, null))
.isInstanceOf(QBadRequestException.class)
.hasMessageContaining("Records to run through this process were not specified");
Serializable emptyResult = apiScriptUtils.runProcess(TestUtils.PROCESS_NAME_TRANSFORM_PEOPLE, JsonUtils.toJson(Map.of("id", 999)));
assertThat(emptyResult).isInstanceOf(List.class);
assertEquals(0, ((List<?>) emptyResult).size());
Serializable result = apiScriptUtils.runProcess(TestUtils.PROCESS_NAME_TRANSFORM_PEOPLE, JsonUtils.toJson(Map.of("id", "1,2,3")));
assertThat(result).isInstanceOf(List.class);
List<Map<String, Object>> resultList = (List<Map<String, Object>>) result;
assertEquals(3, resultList.size());
assertThat(resultList.stream().filter(m -> m.get("id").equals(2)).findFirst()).isPresent().get().hasFieldOrPropertyWithValue("statusCode", 200);
assertThat(resultList.stream().filter(m -> m.get("id").equals(3)).findFirst()).isPresent().get().hasFieldOrPropertyWithValue("statusCode", 500);
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testAsyncProcessAndGetStatus() throws QException
{
insertSimpsons();
ApiScriptUtils apiScriptUtils = newDefaultApiScriptUtils();
Serializable asyncResult = apiScriptUtils.runProcess(TestUtils.PROCESS_NAME_TRANSFORM_PEOPLE, JsonUtils.toJson(Map.of("id", "1,2,3", "async", true)));
assertThat(asyncResult).isInstanceOf(Map.class);
String jobId = ValueUtils.getValueAsString(((Map<String, ?>) asyncResult).get("jobId"));
assertNotNull(jobId);
SleepUtils.sleep(100, TimeUnit.MILLISECONDS);
Serializable result = apiScriptUtils.getProcessStatus(TestUtils.PROCESS_NAME_TRANSFORM_PEOPLE, jobId);
assertThat(result).isInstanceOf(List.class);
List<Map<String, Object>> resultList = (List<Map<String, Object>>) result;
assertEquals(3, resultList.size());
assertThat(resultList.stream().filter(m -> m.get("id").equals(2)).findFirst()).isPresent().get().hasFieldOrPropertyWithValue("statusCode", 200);
assertThat(resultList.stream().filter(m -> m.get("id").equals(3)).findFirst()).isPresent().get().hasFieldOrPropertyWithValue("statusCode", 500);
}
/*******************************************************************************
**
*******************************************************************************/