diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/async/AsyncJobManager.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/async/AsyncJobManager.java index 2c16afd0..14b9fad6 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/async/AsyncJobManager.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/async/AsyncJobManager.java @@ -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 startJob(String jobName, long timeout, TimeUnit timeUnit, AsyncJob 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); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java index efa03b2e..b9bb2d47 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java @@ -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) { diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/ProcessSummaryFilterLink.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/ProcessSummaryFilterLink.java index b13e69e3..eee92ff2 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/ProcessSummaryFilterLink.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/ProcessSummaryFilterLink.java @@ -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()); + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/ProcessSummaryLine.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/ProcessSummaryLine.java index 39ead861..786318e0 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/ProcessSummaryLine.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/ProcessSummaryLine.java @@ -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, "")); } } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/ProcessSummaryRecordLink.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/ProcessSummaryRecordLink.java index fa8ae313..50e2c378 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/ProcessSummaryRecordLink.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/ProcessSummaryRecordLink.java @@ -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()); + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QSupplementalProcessMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QSupplementalProcessMetaData.java index 5a478053..0a5dce8b 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QSupplementalProcessMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QSupplementalProcessMetaData.java @@ -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 // + //////////////////////// + } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLPreviewStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLPreviewStep.java index 99045d3a..4921c9ce 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLPreviewStep.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLPreviewStep.java @@ -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); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ExceptionUtils.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ExceptionUtils.java index c4d71714..b0c0817e 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ExceptionUtils.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ExceptionUtils.java @@ -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 messages = new ArrayList<>(); + Throwable root = exception; + Set 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)); + } } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/ExceptionUtilsTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/ExceptionUtilsTest.java index c92d4fe5..4b68f28b 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/ExceptionUtilsTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/ExceptionUtilsTest.java @@ -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; diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/ApiImplementation.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/ApiImplementation.java index 8a2f7dcd..315d9ed4 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/ApiImplementation.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/ApiImplementation.java @@ -30,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; @@ -119,7 +127,7 @@ public class ApiImplementation /////////////////////////////////////////////////////////////////// // key: Pair, value: Map metaData> // /////////////////////////////////////////////////////////////////// - private static Map, Map> tableApiNameMap = new HashMap<>(); + private static Map, Map> tableApiNameMap = new HashMap<>(); @@ -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,24 +954,27 @@ 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) { - if(apiProcessInput != null && apiProcessInput.getRecordIdsParamName() != null) + String idParam = apiProcessInput.getRecordIdsParamName(); + if(StringUtils.hasContent(idParam) && StringUtils.hasContent(paramMap.get(idParam))) { - String idParam = apiProcessInput.getRecordIdsParamName(); - if(StringUtils.hasContent(idParam) && StringUtils.hasContent(paramMap.get(idParam))) - { - String[] ids = paramMap.get(idParam).split(","); + String[] ids = paramMap.get(idParam).split(","); - QTableMetaData table = QContext.getQInstance().getTable(process.getTableName()); - QQueryFilter filter = new QQueryFilter(new QFilterCriteria(table.getPrimaryKeyField(), IN, Arrays.asList(ids))); - runProcessInput.setCallback(getCallback(filter)); - } + QTableMetaData table = QContext.getQInstance().getTable(process.getTableName()); + QQueryFilter filter = new QQueryFilter(new QFilterCriteria(table.getPrimaryKeyField(), IN, Arrays.asList(ids))); + runProcessInput.setCallback(getCallback(filter)); } } @@ -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 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 customizers = apiProcessMetaData.getCustomizers(); if(customizers != null && customizers.containsKey(ApiProcessCustomizers.POST_RUN.getRole())) { PostRunApiProcessCustomizer postRunCustomizer = QCodeLoader.getAdHoc(PostRunApiProcessCustomizer.class, customizers.get(ApiProcessCustomizers.POST_RUN.getRole())); @@ -1044,16 +1109,101 @@ public class ApiImplementation for(QFieldMetaData inputField : CollectionUtils.nonNullList(fieldsContainer.getFields())) { - String value = paramMap.get(inputField.getName()); - if(!StringUtils.hasContent(value) && inputField.getIsRequired()) + processSingleProcessInputField(inputField, paramMap, badRequestMessages, runProcessInput); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static void processSingleProcessInputField(QFieldMetaData inputField, Map paramMap, List badRequestMessages, RunProcessInput runProcessInput) + { + String value = paramMap.get(inputField.getName()); + + if(!StringUtils.hasContent(value) && inputField.getDefaultValue() != null) + { + value = ValueUtils.getValueAsString(inputField.getDefaultValue()); + } + + if(!StringUtils.hasContent(value) && inputField.getIsRequired()) + { + badRequestMessages.add("Missing value for required input field " + inputField.getName()); + return; + } + + // todo - types? + + runProcessInput.addValue(inputField.getName(), value); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static HttpApiResponse getProcessStatus(ApiInstanceMetaData apiInstanceMetaData, String version, String apiProcessName, String jobUUID) throws QException + { + Optional 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 = RunProcessAction.getState(jobUUID); + if(processState.isPresent()) { - badRequestMessages.add("Missing value for required input field " + inputField.getName()); - continue; + RunProcessOutput runProcessOutput = new RunProcessOutput(processState.get()); + RunProcessInput runProcessInput = new RunProcessInput(); + runProcessInput.seedFromProcessState(processState.get()); + + Pair pair = ApiProcessUtils.getProcessMetaDataPair(apiInstanceMetaData, version, apiProcessName); + + ApiProcessMetaData apiProcessMetaData = pair.getA(); + return (buildResponseAfterProcess(apiProcessMetaData, runProcessInput, runProcessOutput)); } - - // todo - types? - - runProcessInput.addValue(inputField.getName(), value); + 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 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 - /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/GenerateOpenApiSpecAction.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/GenerateOpenApiSpecAction.java index a37efeac..d38e5386 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/GenerateOpenApiSpecAction.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/GenerateOpenApiSpecAction.java @@ -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 tagList = new ArrayList<>(); + Set usedProcessNames = new HashSet<>(); + /////////////////// // foreach table // /////////////////// - List tables = new ArrayList<>(qInstance.getTables().values()); - Set 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 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 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> processesNotUnderTables = getProcessesNotUnderTables(apiName, apiVersion, usedProcessNames); - for(Pair pair : CollectionUtils.nonNullList(processesNotUnderTables)) + if(input.getTableName() == null) { - ApiProcessMetaData apiProcessMetaData = pair.getA(); - QProcessMetaData processMetaData = pair.getB(); + List> processesNotUnderTables = getProcessesNotUnderTables(apiName, apiVersion, usedProcessNames); + for(Pair 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)); - usedProcessNames.add(processMetaData.getName()); + 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 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 parameters = new ArrayList<>(); ApiProcessInput apiProcessInput = apiProcessMetaData.getInput(); if(apiProcessInput != null) @@ -806,17 +885,67 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction 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 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 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 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,9 +319,11 @@ public class QJavalinApiHandler setupSession(context, null, version, apiInstanceMetaData); QJavalinAccessLogger.logStart("apiRunProcess", logPair("process", processMetaData.getName())); + //////////////////////////////////////////////////// + // process inputs into map for api implementation // + //////////////////////////////////////////////////// Map parameters = new LinkedHashMap<>(); - - ApiProcessInput input = apiProcessMetaData.getInput(); + ApiProcessInput input = apiProcessMetaData.getInput(); if(input != null) { processProcessInputFieldsContainer(context, parameters, input.getQueryStringParams(), Context::queryParam); @@ -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; } } diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/fields/ApiFieldMetaData.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/fields/ApiFieldMetaData.java index fabc02f0..d03c7fa4 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/fields/ApiFieldMetaData.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/fields/ApiFieldMetaData.java @@ -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 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 getExamples() + { + return (this.examples); + } + + + + /******************************************************************************* + ** Setter for examples + *******************************************************************************/ + public void setExamples(Map examples) + { + this.examples = examples; + } + + + + /******************************************************************************* + ** Fluent setter for examples + *******************************************************************************/ + public ApiFieldMetaData withExamples(Map examples) + { + this.examples = examples; + return (this); + } + } diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/fields/ApiFieldMetaDataContainer.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/fields/ApiFieldMetaDataContainer.java index cc6e62d2..fa9fe94a 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/fields/ApiFieldMetaDataContainer.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/fields/ApiFieldMetaDataContainer.java @@ -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 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); + } + } diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessInput.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessInput.java index f533b9ae..83b05691 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessInput.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessInput.java @@ -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 . + */ + 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); + } + } diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessInputFieldsContainer.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessInputFieldsContainer.java index 7c5b0060..67e2958f 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessInputFieldsContainer.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessInputFieldsContainer.java @@ -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 . + */ + 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) { - fields = new ArrayList<>(); + 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 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); + } + } } } diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessMetaData.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessMetaData.java index b3cfe92a..9cb32dbd 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessMetaData.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessMetaData.java @@ -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); + } + } diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessMetaDataContainer.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessMetaDataContainer.java index 5fe3351a..496e8ea0 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessMetaDataContainer.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessMetaDataContainer.java @@ -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 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); } - } diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessObjectOutput.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessObjectOutput.java index e92556a6..20cf7453 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessObjectOutput.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessObjectOutput.java @@ -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 . + */ + 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 outputFields; + private String responseDescription; + private HttpStatus.Code successResponseCode; + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public HttpStatus.Code getSuccessStatusCode(RunProcessInput runProcessInput, RunProcessOutput runProcessOutput) + { + return (HttpStatus.Code.OK); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public Map getSpecResponses(String apiName) + { + Map 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); + } + } diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessOutputInterface.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessOutputInterface.java index 476bb072..6e430aa4 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessOutputInterface.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessOutputInterface.java @@ -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 . + */ + 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 getSpecResponses(String apiName) + { + return (MapBuilder.of( + HttpStatus.Code.NO_CONTENT.getCode(), new Response() + .withDescription("Process has been successfully executed.") + )); + } } diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessSummaryListOutput.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessSummaryListOutput.java index 9894f96a..847e4b8c 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessSummaryListOutput.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessSummaryListOutput.java @@ -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 . + */ + 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 getSpecResponses(String apiName) + { + Map 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 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 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 toMap(ProcessSummaryFilterLink processSummaryFilterLink) + { + HashMap map = initResultMapForProcessSummaryLine(processSummaryFilterLink); + + String messagePrefix = getResultMapMessagePrefix(processSummaryFilterLink); + map.put("message", messagePrefix + processSummaryFilterLink.getFullText()); + + return (map); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private HashMap toMap(ProcessSummaryRecordLink processSummaryRecordLink) + { + HashMap map = initResultMapForProcessSummaryLine(processSummaryRecordLink); + + String messagePrefix = getResultMapMessagePrefix(processSummaryRecordLink); + map.put("message", messagePrefix + processSummaryRecordLink.getFullText()); + + return (map); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ private static HashMap toMap(ProcessSummaryLine processSummaryLine) + { + HashMap 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 initResultMapForProcessSummaryLine(ProcessSummaryLineInterface processSummaryLine) { HashMap 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; } } diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessUtils.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessUtils.java index dd1a1321..b69797e4 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessUtils.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessUtils.java @@ -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 . + */ + package com.kingsrook.qqq.api.model.metadata.processes; diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/PostRunApiProcessCustomizer.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/PostRunApiProcessCustomizer.java index 06ba64f8..e9fca81a 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/PostRunApiProcessCustomizer.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/PostRunApiProcessCustomizer.java @@ -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 . + */ + package com.kingsrook.qqq.api.model.metadata.processes; diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/PreRunApiProcessCustomizer.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/PreRunApiProcessCustomizer.java index e1836708..a463af66 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/PreRunApiProcessCustomizer.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/PreRunApiProcessCustomizer.java @@ -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 . + */ + package com.kingsrook.qqq.api.model.metadata.processes; diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/ExampleWithSingleValue.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/ExampleWithSingleValue.java index e365e16b..79ebab6c 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/ExampleWithSingleValue.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/ExampleWithSingleValue.java @@ -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); diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/Parameter.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/Parameter.java index 4077f25b..9d43451f 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/Parameter.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/Parameter.java @@ -37,6 +37,7 @@ public class Parameter private Schema schema; private Boolean explode; private Map 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); + } + } diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/Schema.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/Schema.java index 228bae49..3f858881 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/Schema.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/Schema.java @@ -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 *******************************************************************************/ diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/utils/ApiScriptUtils.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/utils/ApiScriptUtils.java index f06585d2..48056e27 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/utils/ApiScriptUtils.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/utils/ApiScriptUtils.java @@ -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 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()); + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-middleware-api/src/main/resources/rapidoc/rapidoc-container.html b/qqq-middleware-api/src/main/resources/rapidoc/rapidoc-container.html index 8be1775d..540a5a24 100644 --- a/qqq-middleware-api/src/main/resources/rapidoc/rapidoc-container.html +++ b/qqq-middleware-api/src/main/resources/rapidoc/rapidoc-container.html @@ -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" diff --git a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/TestUtils.java b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/TestUtils.java index 20ea1005..fea81122 100644 --- a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/TestUtils.java +++ b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/TestUtils.java @@ -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() diff --git a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandlerTest.java b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandlerTest.java index 3db79362..c3d3e339 100644 --- a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandlerTest.java +++ b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandlerTest.java @@ -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 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 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()); + } + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/utils/ApiScriptUtilsTest.java b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/utils/ApiScriptUtilsTest.java index ec4cfb2b..fdc06e63 100644 --- a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/utils/ApiScriptUtilsTest.java +++ b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/utils/ApiScriptUtilsTest.java @@ -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> resultList = (List>) 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) 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> resultList = (List>) 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); + } + + + /******************************************************************************* ** *******************************************************************************/