Checkpoint, WIP on processes in api - mostly done

This commit is contained in:
2023-06-14 11:53:43 -05:00
parent eee7354e77
commit 19ee5bcb23
32 changed files with 1875 additions and 176 deletions

View File

@ -39,6 +39,7 @@ import com.kingsrook.qqq.backend.core.state.InMemoryStateProvider;
import com.kingsrook.qqq.backend.core.state.StateProviderInterface;
import com.kingsrook.qqq.backend.core.state.StateType;
import com.kingsrook.qqq.backend.core.state.UUIDAndTypeStateKey;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import org.apache.logging.log4j.Level;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
@ -50,6 +51,7 @@ public class AsyncJobManager
{
private static final QLogger LOG = QLogger.getLogger(AsyncJobManager.class);
private String forcedJobUUID = null;
/*******************************************************************************
@ -69,7 +71,8 @@ public class AsyncJobManager
*******************************************************************************/
public <T extends Serializable> T startJob(String jobName, long timeout, TimeUnit timeUnit, AsyncJob<T> asyncJob) throws JobGoingAsyncException, QException
{
UUIDAndTypeStateKey uuidAndTypeStateKey = new UUIDAndTypeStateKey(UUID.randomUUID(), StateType.ASYNC_JOB_STATUS);
UUID jobUUID = StringUtils.hasContent(forcedJobUUID) ? UUID.fromString(forcedJobUUID) : UUID.randomUUID();
UUIDAndTypeStateKey uuidAndTypeStateKey = new UUIDAndTypeStateKey(jobUUID, StateType.ASYNC_JOB_STATUS);
AsyncJobStatus asyncJobStatus = new AsyncJobStatus();
asyncJobStatus.setState(AsyncJobState.RUNNING);
getStateProvider().put(uuidAndTypeStateKey, asyncJobStatus);
@ -205,4 +208,35 @@ public class AsyncJobManager
jobStatus.ifPresent(asyncJobStatus -> asyncJobStatus.setCancelRequested(true));
}
/*******************************************************************************
** Getter for forcedJobUUID
*******************************************************************************/
public String getForcedJobUUID()
{
return (this.forcedJobUUID);
}
/*******************************************************************************
** Setter for forcedJobUUID
*******************************************************************************/
public void setForcedJobUUID(String forcedJobUUID)
{
this.forcedJobUUID = forcedJobUUID;
}
/*******************************************************************************
** Fluent setter for forcedJobUUID
*******************************************************************************/
public AsyncJobManager withForcedJobUUID(String forcedJobUUID)
{
this.forcedJobUUID = forcedJobUUID;
return (this);
}
}

View File

@ -63,6 +63,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppSection;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QSupplementalProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.queues.SQSQueueProviderMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportDataSource;
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportField;
@ -1226,6 +1227,11 @@ public class QInstanceValidator
}
}
for(QSupplementalProcessMetaData supplementalProcessMetaData : CollectionUtils.nonNullMap(process.getSupplementalMetaData()).values())
{
supplementalProcessMetaData.validate(qInstance, process, this);
}
});
}
}
@ -1703,7 +1709,7 @@ public class QInstanceValidator
** But if it throws, add the provided message to the list of errors (and return false,
** e.g., in case you need to stop evaluating rules to avoid exceptions).
*******************************************************************************/
private boolean assertNoException(UnsafeLambda unsafeLambda, String message)
public boolean assertNoException(UnsafeLambda unsafeLambda, String message)
{
try
{
@ -1736,7 +1742,7 @@ public class QInstanceValidator
/*******************************************************************************
**
*******************************************************************************/
private void warn(String message)
public void warn(String message)
{
if(printWarnings)
{

View File

@ -22,8 +22,10 @@
package com.kingsrook.qqq.backend.core.model.actions.processes;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.kingsrook.qqq.backend.core.logging.LogPair;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
@ -76,6 +78,35 @@ public class ProcessSummaryFilterLink implements ProcessSummaryLineInterface
/*******************************************************************************
**
*******************************************************************************/
@JsonIgnore
public String getFullText()
{
StringBuilder rs = new StringBuilder();
if(StringUtils.hasContent(linkPreText))
{
rs.append(linkPreText).append(" ");
}
if(StringUtils.hasContent(linkText))
{
rs.append(linkText).append(" ");
}
if(StringUtils.hasContent(linkPostText))
{
rs.append(linkPostText).append(" ");
}
rs.deleteCharAt(rs.length() - 1);
return (rs.toString());
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -26,6 +26,7 @@ import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qqq.backend.core.logging.LogPair;
import com.kingsrook.qqq.backend.core.utils.ObjectUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
@ -395,15 +396,19 @@ public class ProcessSummaryLine implements ProcessSummaryLineInterface
{
if(count != null)
{
String baseMessage;
if(count.equals(1))
{
setMessage((isPast ? getSingularPastMessage() : getSingularFutureMessage())
+ (messageSuffix == null ? "" : messageSuffix));
baseMessage = isPast ? getSingularPastMessage() : getSingularFutureMessage();
}
else
{
setMessage((isPast ? getPluralPastMessage() : getPluralFutureMessage())
+ (messageSuffix == null ? "" : messageSuffix));
baseMessage = isPast ? getPluralPastMessage() : getPluralFutureMessage();
}
if(StringUtils.hasContent(baseMessage))
{
setMessage(baseMessage + ObjectUtils.requireConditionElse(messageSuffix, StringUtils::hasContent, ""));
}
}
}

View File

@ -23,7 +23,9 @@ package com.kingsrook.qqq.backend.core.model.actions.processes;
import java.io.Serializable;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.kingsrook.qqq.backend.core.logging.LogPair;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
@ -64,6 +66,35 @@ public class ProcessSummaryRecordLink implements ProcessSummaryLineInterface
/*******************************************************************************
**
*******************************************************************************/
@JsonIgnore
public String getFullText()
{
StringBuilder rs = new StringBuilder();
if(StringUtils.hasContent(linkPreText))
{
rs.append(linkPreText).append(" ");
}
if(StringUtils.hasContent(linkText))
{
rs.append(linkText).append(" ");
}
if(StringUtils.hasContent(linkPostText))
{
rs.append(linkPostText).append(" ");
}
rs.deleteCharAt(rs.length() - 1);
return (rs.toString());
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -23,6 +23,8 @@ package com.kingsrook.qqq.backend.core.model.metadata.processes;
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;
/*******************************************************************************
@ -75,4 +77,16 @@ public abstract class QSupplementalProcessMetaData
// noop in base class //
////////////////////////
}
/*******************************************************************************
**
*******************************************************************************/
public void validate(QInstance qInstance, QProcessMetaData process, QInstanceValidator qInstanceValidator)
{
////////////////////////
// noop in base class //
////////////////////////
}
}

View File

@ -27,6 +27,7 @@ import java.util.List;
import com.kingsrook.qqq.backend.core.actions.async.AsyncRecordPipeLoop;
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
import com.kingsrook.qqq.backend.core.actions.reporting.RecordPipe;
import com.kingsrook.qqq.backend.core.context.QContext;
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.RunBackendStepInput;
@ -71,11 +72,14 @@ public class StreamedETLPreviewStep extends BaseStreamedETLStep implements Backe
return;
}
if(runBackendStepInput.getFrontendStepBehavior() != null && runBackendStepInput.getFrontendStepBehavior().equals(RunProcessInput.FrontendStepBehavior.SKIP))
{
LOG.debug("Skipping preview because frontend behavior is [" + RunProcessInput.FrontendStepBehavior.SKIP + "].");
return;
}
//////////////////////////////
// set up the extract steps //
//////////////////////////////
AbstractExtractStep extractStep = getExtractStep(runBackendStepInput);
RecordPipe recordPipe = new RecordPipe();
extractStep.setLimit(limit);
extractStep.setRecordPipe(recordPipe);
extractStep.preRun(runBackendStepInput, runBackendStepOutput);
/////////////////////////////////////////////////////////////////
// if we're running inside an automation, then skip this step. //
@ -86,17 +90,26 @@ public class StreamedETLPreviewStep extends BaseStreamedETLStep implements Backe
return;
}
//////////////////////////////////////////
// set up the extract & transform steps //
//////////////////////////////////////////
AbstractExtractStep extractStep = getExtractStep(runBackendStepInput);
RecordPipe recordPipe = new RecordPipe();
extractStep.setLimit(limit);
extractStep.setRecordPipe(recordPipe);
extractStep.preRun(runBackendStepInput, runBackendStepOutput);
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if skipping frontend steps, skip this action - //
// but, if inside an (ideally, only async) API call, at least do the count, so status calls can get x of y status //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
if(RunProcessInput.FrontendStepBehavior.SKIP.equals(runBackendStepInput.getFrontendStepBehavior()))
{
if(QContext.getQSession().getValue("apiVersion") != null)
{
countRecords(runBackendStepInput, runBackendStepOutput, extractStep);
}
LOG.debug("Skipping preview because frontend behavior is [" + RunProcessInput.FrontendStepBehavior.SKIP + "].");
return;
}
countRecords(runBackendStepInput, runBackendStepOutput, extractStep);
//////////////////////////////
// setup the transform step //
//////////////////////////////
AbstractTransformStep transformStep = getTransformStep(runBackendStepInput);
transformStep.preRun(runBackendStepInput, runBackendStepOutput);

View File

@ -22,7 +22,9 @@
package com.kingsrook.qqq.backend.core.utils;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@ -88,4 +90,39 @@ public class ExceptionUtils
return (root);
}
/*******************************************************************************
**
*******************************************************************************/
public static String concatenateMessagesFromChain(Exception exception)
{
if(exception == null)
{
return (null);
}
List<String> messages = new ArrayList<>();
Throwable root = exception;
Set<Throwable> seen = new HashSet<>();
do
{
if(StringUtils.hasContent(root.getMessage()))
{
messages.add(root.getMessage());
}
else
{
messages.add(root.getClass().getSimpleName());
}
seen.add(root);
root = root.getCause();
}
while(root != null && !seen.contains(root));
return (StringUtils.join("; ", messages));
}
}

View File

@ -26,6 +26,7 @@ import com.kingsrook.qqq.backend.core.BaseTest;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
@ -88,6 +89,33 @@ class ExceptionUtilsTest extends BaseTest
/*******************************************************************************
**
*******************************************************************************/
@Test
void testConcatenateMessagesFromChain()
{
assertNull(ExceptionUtils.concatenateMessagesFromChain(null));
assertEquals("QException", ExceptionUtils.concatenateMessagesFromChain(new QException((String) null)));
assertEquals("QException", ExceptionUtils.concatenateMessagesFromChain(new QException("")));
assertEquals("foo; bar", ExceptionUtils.concatenateMessagesFromChain(new QException("foo", new QException("bar"))));
assertEquals("foo; QException; bar", ExceptionUtils.concatenateMessagesFromChain(new QException("foo", new QException(null, new QException("bar")))));
MyException selfCaused = new MyException("selfCaused");
selfCaused.setCause(selfCaused);
assertEquals("selfCaused", ExceptionUtils.concatenateMessagesFromChain(selfCaused));
MyException cycle1 = new MyException("cycle1");
MyException cycle2 = new MyException("cycle2");
cycle1.setCause(cycle2);
cycle2.setCause(cycle1);
assertEquals("cycle1; cycle2", ExceptionUtils.concatenateMessagesFromChain(cycle1));
assertEquals("cycle2; cycle1", ExceptionUtils.concatenateMessagesFromChain(cycle2));
}
/*******************************************************************************
** Test exception class - lets you set the cause, easier to create a loop.
*******************************************************************************/
@ -97,6 +125,9 @@ class ExceptionUtilsTest extends BaseTest
/*******************************************************************************
**
*******************************************************************************/
public MyException(String message)
{
super(message);
@ -104,6 +135,9 @@ class ExceptionUtilsTest extends BaseTest
/*******************************************************************************
**
*******************************************************************************/
public MyException(Throwable cause)
{
super(cause);
@ -111,6 +145,9 @@ class ExceptionUtilsTest extends BaseTest
/*******************************************************************************
**
*******************************************************************************/
public void setCause(Throwable cause)
{
myCause = cause;

View File

@ -30,8 +30,10 @@ 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;
@ -47,6 +49,10 @@ import com.kingsrook.qqq.api.model.metadata.processes.PostRunApiProcessCustomize
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;
@ -63,6 +69,7 @@ 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;
@ -96,6 +103,7 @@ 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;
@ -306,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)));
}
}
@ -946,13 +954,17 @@ public class ApiImplementation
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(process.getMinInputRecords() != null && process.getMinInputRecords() > 0)
if(apiProcessInput != null && apiProcessInput.getRecordIdsParamName() != null)
{
String idParam = apiProcessInput.getRecordIdsParamName();
@ -965,7 +977,6 @@ public class ApiImplementation
runProcessInput.setCallback(getCallback(filter));
}
}
}
/////////////////////////////////////////
// throw if bad inputs have been noted //
@ -978,7 +989,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)));
}
}
@ -992,6 +1003,44 @@ public class ApiImplementation
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 //
/////////////////////
@ -1006,10 +1055,26 @@ public class ApiImplementation
{
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()));
@ -1043,18 +1108,103 @@ public class ApiImplementation
}
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());
continue;
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));
}
}
@ -1330,7 +1480,6 @@ public class ApiImplementation
/*******************************************************************************
**
*******************************************************************************/

View File

@ -41,10 +41,12 @@ 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;
@ -90,6 +92,7 @@ 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;
@ -296,13 +299,13 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
.withDescription("Requested result page size")
)));
List<Tag> tagList = new ArrayList<>();
Set<String> usedProcessNames = new HashSet<>();
///////////////////
// foreach table //
///////////////////
List<QTableMetaData> tables = new ArrayList<>(qInstance.getTables().values());
Set<String> usedProcessNames = new HashSet<>();
tables.sort(Comparator.comparing(t -> ObjectUtils.requireNonNullElse(t.getLabel(), t.getName(), "")));
for(QTableMetaData table : tables)
for(QTableMetaData table : qInstance.getTables().values())
{
String tableName = table.getName();
@ -385,6 +388,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 //
///////////////////////////////
@ -416,13 +423,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 //
//////////////////////////////////////
@ -557,6 +557,34 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
.withSchema(new Schema().withRef("#/components/schemas/" + tableApiName)))))
.withSecurity(getSecurity(apiInstanceMetaData, tableReadPermissionName));
Method slashPost = new Method()
.withSummary("Create one " + tableLabel)
.withDescription(INSERT_DESCRIPTION)
.withOperationId("create" + tableApiNameUcFirst)
.withRequestBody(new RequestBody()
.withRequired(true)
.withDescription("Values for the " + tableLabel + " record to create.")
.withContent(MapBuilder.of("application/json", new Content()
.withSchema(new Schema().withRef("#/components/schemas/" + tableApiName))
)))
.withResponses(buildStandardErrorResponses(apiInstanceMetaData))
.withResponse(HttpStatus.CREATED.getCode(), new Response()
.withDescription("Successfully created the requested " + tableLabel)
.withContent(MapBuilder.of("application/json", new Content()
.withSchema(new Schema()
.withType("object")
.withProperties(MapBuilder.of(primaryKeyApiName, new Schema()
.withType(getFieldType(primaryKeyField))
.withExample("47")))))))
.withTags(ListBuilder.of(tableLabel))
.withSecurity(getSecurity(apiInstanceMetaData, tableInsertPermissionName));
if(insertEnabled)
{
openAPI.getPaths().put(basePath + tableApiName + "/", new Path()
.withPost(slashPost));
}
Method idPatch = new Method()
.withSummary("Update one " + tableLabel)
.withDescription(UPDATE_DESCRIPTION)
@ -605,34 +633,6 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
);
}
Method slashPost = new Method()
.withSummary("Create one " + tableLabel)
.withDescription(INSERT_DESCRIPTION)
.withOperationId("create" + tableApiNameUcFirst)
.withRequestBody(new RequestBody()
.withRequired(true)
.withDescription("Values for the " + tableLabel + " record to create.")
.withContent(MapBuilder.of("application/json", new Content()
.withSchema(new Schema().withRef("#/components/schemas/" + tableApiName))
)))
.withResponses(buildStandardErrorResponses(apiInstanceMetaData))
.withResponse(HttpStatus.CREATED.getCode(), new Response()
.withDescription("Successfully created the requested " + tableLabel)
.withContent(MapBuilder.of("application/json", new Content()
.withSchema(new Schema()
.withType("object")
.withProperties(MapBuilder.of(primaryKeyApiName, new Schema()
.withType(getFieldType(primaryKeyField))
.withExample("47")))))))
.withTags(ListBuilder.of(tableLabel))
.withSecurity(getSecurity(apiInstanceMetaData, tableInsertPermissionName));
if(insertEnabled)
{
openAPI.getPaths().put(basePath + tableApiName + "/", new Path()
.withPost(slashPost));
}
////////////////
// bulk paths //
////////////////
@ -699,15 +699,27 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
.withDelete(deleteBulkEnabled ? bulkDelete : null));
}
// todo - need to place these differently based on something?
///////////////////////////////////////
// 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();
String processApiPath = ApiProcessUtils.getProcessApiPath(qInstance, processMetaData, apiProcessMetaData, apiInstanceMetaData);
Path path = generateProcessSpecPathObject(apiInstanceMetaData, apiProcessMetaData, processMetaData, ListBuilder.of(tableLabel));
openAPI.getPaths().put(basePath + processApiPath, path);
addProcessEndpoints(qInstance, apiInstanceMetaData, basePath, openAPI, tableProcessesTag, apiProcessMetaData, processMetaData);
usedProcessNames.add(processMetaData.getName());
}
@ -716,19 +728,35 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
/////////////////////////////
// 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 processApiPath = ApiProcessUtils.getProcessApiPath(qInstance, processMetaData, apiProcessMetaData, apiInstanceMetaData);
Path path = generateProcessSpecPathObject(apiInstanceMetaData, apiProcessMetaData, processMetaData, ListBuilder.of(processMetaData.getLabel()));
openAPI.getPaths().put(basePath + processApiPath, path);
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."));
@ -750,6 +778,41 @@ 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);
}
}
/*******************************************************************************
**
*******************************************************************************/
@ -792,13 +855,29 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
*******************************************************************************/
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(processMetaData.getLabel()) // todo - add optional summary to meta data
.withDescription("Run the process named " + processMetaData.getLabel())// todo - add optional description to meta data, .withDescription()
.withSecurity(getSecurity(apiInstanceMetaData, "todo - process name"));
.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)
@ -806,17 +885,67 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
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(field.getName())
// todo - add description to meta data .withDescription("Which page of results to return. Starts at 1.")
.withDescription("Value for the " + field.getLabel() + " field.")
.withName("async")
.withIn("query")
.withRequired(field.getIsRequired())
.withSchema(new Schema().withType(getFieldType(field))));
}
}
.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))
@ -824,13 +953,33 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
methodForProcess.setParameters(parameters);
}
// todo methodForProcess.withRequestBody();
//////////////////////////////////
// build all possible responses //
//////////////////////////////////
Map<Integer, Response> responses = new LinkedHashMap<>();
// todo methodForProcess.withResponse();
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")
))
)
))
);
}
methodForProcess.withResponse(HttpStatus.OK.getCode(), new Response()
.withDescription("Successfully ran the process")
.withContent(MapBuilder.of("application/json", new Content())));
responses.putAll(buildStandardErrorResponses(apiInstanceMetaData));
methodForProcess.withResponses(responses);
@SuppressWarnings("checkstyle:indentation")
Path path = switch(apiProcessMetaData.getMethod())
@ -847,6 +996,100 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
/*******************************************************************************
**
*******************************************************************************/
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 static 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.";
}
if(apiFieldMetaData != null && StringUtils.hasContent(apiFieldMetaData.getDescription()))
{
description = apiFieldMetaData.getDescription();
}
Parameter parameter = new Parameter()
.withName(field.getName())
.withDescription(description)
.withRequired(field.getIsRequired())
.withSchema(new Schema().withType(getFieldType(field)));
if(apiFieldMetaData != null)
{
if(apiFieldMetaData.getExample() != null)
{
parameter.withExample(apiFieldMetaData.getExample());
}
else if(apiFieldMetaData.getExamples() != null)
{
parameter.withExamples(apiFieldMetaData.getExamples());
}
}
return (parameter);
}
/*******************************************************************************
**
*******************************************************************************/
@ -1337,7 +1580,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
/*******************************************************************************
**
*******************************************************************************/
private String getFieldType(QFieldMetaData field)
public static String getFieldType(QFieldMetaData field)
{
return (getFieldType(field.getType()));
}
@ -1348,7 +1591,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

@ -111,7 +111,7 @@ public class QJavalinApiHandler
{
private static final QLogger LOG = QLogger.getLogger(QJavalinApiHandler.class);
private static final ApiProcessMetaDataContainer EMPTY_CONTAINER = new ApiProcessMetaDataContainer().withApis(Collections.emptyMap());
private static final ApiProcessMetaDataContainer EMPTY_API_PROCESS_META_DATA_CONTAINER = new ApiProcessMetaDataContainer().withApis(Collections.emptyMap());
private static QInstance qInstance;
@ -185,9 +185,10 @@ public class QJavalinApiHandler
///////////////////
for(QProcessMetaData process : qInstance.getProcesses().values())
{
ApiProcessMetaDataContainer apiProcessMetaDataContainer = Objects.requireNonNullElse(ApiProcessMetaDataContainer.of(process), EMPTY_CONTAINER);
ApiProcessMetaDataContainer apiProcessMetaDataContainer = Objects.requireNonNullElse(ApiProcessMetaDataContainer.of(process), EMPTY_API_PROCESS_META_DATA_CONTAINER);
ApiProcessMetaData apiProcessMetaData = apiProcessMetaDataContainer.getApis().get(apiInstanceMetaData.getName());
if(apiProcessMetaData != null)
if(apiProcessMetaData != null && !BooleanUtils.isTrue(apiProcessMetaData.getIsExcluded()))
{
String path = ApiProcessUtils.getProcessApiPath(qInstance, process, apiProcessMetaData, apiInstanceMetaData);
HttpMethod method = apiProcessMetaData.getMethod();
@ -203,9 +204,9 @@ public class QJavalinApiHandler
make405sForOtherMethods(method, path);
if(doesProcessSupportAsync(apiInstanceMetaData, process))
if(!ApiProcessMetaData.AsyncMode.NEVER.equals(apiProcessMetaData.getAsyncMode()))
{
ApiBuilder.get(path + "/status/{processId}", context -> getProcessStatus(context, apiInstanceMetaData));
ApiBuilder.get(path + "/status/{jobId}", context -> getProcessStatus(context, process, apiProcessMetaData, apiInstanceMetaData));
}
}
}
@ -304,16 +305,6 @@ public class QJavalinApiHandler
/*******************************************************************************
**
*******************************************************************************/
private void getProcessStatus(Context context, ApiInstanceMetaData apiInstanceMetaData)
{
// todo!
}
/*******************************************************************************
**
*******************************************************************************/
@ -328,8 +319,10 @@ public class QJavalinApiHandler
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)
{
@ -342,12 +335,62 @@ public class QJavalinApiHandler
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());
}
}
HttpApiResponse response = ApiImplementation.runProcess(apiInstanceMetaData, version, apiProcessMetaData.getApiProcessName(), parameters);
context.status(response.getStatusCode().getCode());
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));
@ -384,17 +427,6 @@ public class QJavalinApiHandler
/*******************************************************************************
**
*******************************************************************************/
private boolean doesProcessSupportAsync(ApiInstanceMetaData apiInstanceMetaData, QProcessMetaData process)
{
// todo - implement
return false;
}
/*******************************************************************************
**
*******************************************************************************/
@ -1348,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

@ -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,6 +24,7 @@ package com.kingsrook.qqq.api.model.metadata.fields;
import java.util.LinkedHashMap;
import java.util.Map;
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.QSupplementalFieldMetaData;
@ -36,6 +37,7 @@ public class ApiFieldMetaDataContainer extends QSupplementalFieldMetaData
{
private Map<String, ApiFieldMetaData> apis;
private ApiFieldMetaData defaultApiFieldMetaData;
/*******************************************************************************
@ -59,6 +61,17 @@ public class ApiFieldMetaDataContainer extends QSupplementalFieldMetaData
/*******************************************************************************
** 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));
}
/*******************************************************************************
** Getter for apis
*******************************************************************************/
@ -70,16 +83,16 @@ public class ApiFieldMetaDataContainer extends QSupplementalFieldMetaData
/*******************************************************************************
** 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 QSupplementalFieldMetaData
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

@ -1,6 +1,30 @@
/*
* 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;
/*******************************************************************************
**
*******************************************************************************/
@ -10,6 +34,9 @@ public class ApiProcessInput
private ApiProcessInputFieldsContainer formParams;
private ApiProcessInputFieldsContainer recordBodyParams;
private QFieldMetaData bodyField;
private String bodyFieldContentType;
/*******************************************************************************
@ -127,4 +154,67 @@ public class ApiProcessInput
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

@ -1,7 +1,30 @@
/*
* 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;
@ -21,16 +44,38 @@ public class ApiProcessInputFieldsContainer
/*******************************************************************************
**
** 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)
{
fields.addAll(frontendStep.getInputFields());
for(QFieldMetaData inputField : frontendStep.getInputFields())
{
if(minusFieldNames != null && !minusFieldNames.contains(inputField.getName()))
{
fields.add(inputField);
}
}
}
}

View File

@ -31,12 +31,15 @@ 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;
/*******************************************************************************
@ -52,6 +55,10 @@ public class ApiProcessMetaData
private String path;
private HttpMethod method;
private String summary;
private String description;
private AsyncMode asyncMode = AsyncMode.OPTIONAL;
private ApiProcessInput input;
private ApiProcessOutputInterface output;
@ -60,6 +67,15 @@ public class ApiProcessMetaData
public enum AsyncMode
{
NEVER,
OPTIONAL,
ALWAYS
}
/*******************************************************************************
**
*******************************************************************************/
@ -103,6 +119,10 @@ public class ApiProcessMetaData
enrichFieldList(qInstanceEnricher, apiName, fieldsContainer.getFields());
}
}
if(input.getBodyField() != null)
{
enrichFieldList(qInstanceEnricher, apiName, List.of(input.getBodyField()));
}
}
}
}
@ -446,4 +466,115 @@ public class ApiProcessMetaData
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);
}
}

View File

@ -26,6 +26,8 @@ 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;
@ -46,7 +48,7 @@ public class ApiProcessMetaDataContainer extends QSupplementalProcessMetaData
*******************************************************************************/
public ApiProcessMetaDataContainer()
{
setType("api");
setType(ApiSupplementType.NAME);
}
@ -61,6 +63,23 @@ public class ApiProcessMetaDataContainer extends QSupplementalProcessMetaData
/*******************************************************************************
** 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);
}
/*******************************************************************************
**
*******************************************************************************/
@ -77,6 +96,22 @@ public class ApiProcessMetaDataContainer extends QSupplementalProcessMetaData
/*******************************************************************************
**
*******************************************************************************/
@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
*******************************************************************************/
@ -102,6 +137,22 @@ public class ApiProcessMetaDataContainer extends QSupplementalProcessMetaData
/*******************************************************************************
**
*******************************************************************************/
public ApiProcessMetaData getApiProcessMetaDataOrWithNew(String apiName)
{
ApiProcessMetaData apiProcessMetaData = getApiProcessMetaData(apiName);
if(apiProcessMetaData == null)
{
apiProcessMetaData = new ApiProcessMetaData();
withApiProcessMetaData(apiName, apiProcessMetaData);
}
return (apiProcessMetaData);
}
/*******************************************************************************
** Setter for apis
*******************************************************************************/
@ -135,5 +186,4 @@ public class ApiProcessMetaDataContainer extends QSupplementalProcessMetaData
this.apis.put(apiName, apiProcessMetaData);
return (this);
}
}

View File

@ -1,3 +1,24 @@
/*
* 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;
@ -5,10 +26,25 @@ 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;
/*******************************************************************************
@ -18,6 +54,66 @@ 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))))
));
}
/*******************************************************************************
@ -82,4 +178,66 @@ public class ApiProcessObjectOutput implements ApiProcessOutputInterface
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

@ -1,10 +1,34 @@
/*
* 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;
@ -24,7 +48,17 @@ public interface ApiProcessOutputInterface
*******************************************************************************/
default HttpStatus.Code getSuccessStatusCode(RunProcessInput runProcessInput, RunProcessOutput runProcessOutput)
{
return (HttpStatus.Code.OK);
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

@ -1,10 +1,36 @@
/*
* 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;
@ -14,6 +40,9 @@ import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryReco
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;
@ -52,6 +81,51 @@ public class ApiProcessSummaryListOutput implements ApiProcessOutputInterface
/*******************************************************************************
**
*******************************************************************************/
@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.")
);
}
/*******************************************************************************
**
*******************************************************************************/
@ -67,7 +141,7 @@ public class ApiProcessSummaryListOutput implements ApiProcessOutputInterface
if(processSummaryLineInterface instanceof ProcessSummaryLine processSummaryLine)
{
processSummaryLine.setCount(1);
processSummaryLine.prepareForFrontend(true);
processSummaryLine.pickMessage(true);
List<Serializable> primaryKeys = processSummaryLine.getPrimaryKeys();
if(CollectionUtils.nullSafeHasContents(primaryKeys))
@ -86,11 +160,11 @@ public class ApiProcessSummaryListOutput implements ApiProcessOutputInterface
}
else if(processSummaryLineInterface instanceof ProcessSummaryRecordLink processSummaryRecordLink)
{
throw new NotImplementedException("ProcessSummaryRecordLink handling");
apiOutput.add(toMap(processSummaryRecordLink));
}
else if(processSummaryLineInterface instanceof ProcessSummaryFilterLink processSummaryFilterLink)
{
throw new NotImplementedException("ProcessSummaryFilterLink handling");
apiOutput.add(toMap(processSummaryFilterLink));
}
else
{
@ -112,27 +186,81 @@ public class ApiProcessSummaryListOutput implements ApiProcessOutputInterface
/*******************************************************************************
**
*******************************************************************************/
@SuppressWarnings("checkstyle:indentation")
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;
};
String messagePrefix = switch(processSummaryLine.getStatus())
{
case OK, INFO, ERROR -> "";
case WARNING -> "Warning: ";
};
map.put("statusCode", code.getCode());
map.put("statusText", code.getMessage());
map.put("message", messagePrefix + processSummaryLine.getMessage());
return (map);
return map;
}
}

View File

@ -1,3 +1,24 @@
/*
* 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;

View File

@ -1,3 +1,24 @@
/*
* 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;

View File

@ -1,3 +1,24 @@
/*
* 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;

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

@ -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,11 +30,15 @@ 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.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.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import org.json.JSONObject;
/*******************************************************************************
@ -225,6 +229,53 @@ public class ApiScriptUtils implements 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

@ -189,11 +189,11 @@ public class TestUtils
.withLabel("Person Info Input")
.withComponent(new QFrontendComponentMetaData().withType(QComponentType.EDIT_FORM))
.withFormField(new QFieldMetaData("age", QFieldType.INTEGER))
.withFormField(new QFieldMetaData("age", QFieldType.INTEGER).withIsRequired(true))
.withFormField(new QFieldMetaData("partnerPersonId", QFieldType.INTEGER).withPossibleValueSourceName(TABLE_NAME_PERSON))
.withFormField(new QFieldMetaData("heightInches", QFieldType.DECIMAL))
.withFormField(new QFieldMetaData("weightPounds", QFieldType.INTEGER))
.withFormField(new QFieldMetaData("homeTown", QFieldType.STRING))
.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()

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,6 +69,7 @@ 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;
@ -1448,10 +1451,12 @@ class QJavalinApiHandlerTest extends BaseTest
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());
System.out.println(jsonObject.toString(3));
}
@ -1478,11 +1483,33 @@ class QJavalinApiHandlerTest extends BaseTest
assertEquals(HttpStatus.MULTI_STATUS_207, response.getStatus());
JSONArray jsonArray = new JSONArray(response.getBody());
assertEquals(3, jsonArray.length());
System.out.println(jsonArray.toString(3));
}
/*******************************************************************************
**
*******************************************************************************/
@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);
SleepUtils.sleep(100, TimeUnit.MILLISECONDS);
response = Unirest.get(BASE_URL + "/api/" + VERSION + "/person/transformPeople/status/" + jobId).asString();
assertEquals(HttpStatus.MULTI_STATUS_207, response.getStatus());
JSONArray jsonArray = new JSONArray(response.getBody());
assertEquals(3, jsonArray.length());
}
/*******************************************************************************
**
*******************************************************************************/

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);
}
/*******************************************************************************
**
*******************************************************************************/