diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java index 79fa519e..e66c187d 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java @@ -370,7 +370,7 @@ public class QInstanceEnricher for(QSupplementalProcessMetaData supplementalProcessMetaData : CollectionUtils.nonNullMap(process.getSupplementalMetaData()).values()) { - supplementalProcessMetaData.enrich(process); + supplementalProcessMetaData.enrich(this, process); } enrichPermissionRules(process); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QProcessMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QProcessMetaData.java index 5295cad9..8a1a5eae 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QProcessMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QProcessMetaData.java @@ -54,6 +54,9 @@ public class QProcessMetaData implements QAppChildMetaData, MetaDataWithPermissi private BasepullConfiguration basepullConfiguration; private QPermissionRules permissionRules; + private Integer minInputRecords = null; + private Integer maxInputRecords = null; + private List stepList; // these are the steps that are ran, by-default, in the order they are ran in private Map steps; // this is the full map of possible steps @@ -64,6 +67,7 @@ public class QProcessMetaData implements QAppChildMetaData, MetaDataWithPermissi private Map supplementalMetaData; + /******************************************************************************* ** *******************************************************************************/ @@ -605,4 +609,66 @@ public class QProcessMetaData implements QAppChildMetaData, MetaDataWithPermissi return (this); } + + + /******************************************************************************* + ** Getter for minInputRecords + *******************************************************************************/ + public Integer getMinInputRecords() + { + return (this.minInputRecords); + } + + + + /******************************************************************************* + ** Setter for minInputRecords + *******************************************************************************/ + public void setMinInputRecords(Integer minInputRecords) + { + this.minInputRecords = minInputRecords; + } + + + + /******************************************************************************* + ** Fluent setter for minInputRecords + *******************************************************************************/ + public QProcessMetaData withMinInputRecords(Integer minInputRecords) + { + this.minInputRecords = minInputRecords; + return (this); + } + + + + /******************************************************************************* + ** Getter for maxInputRecords + *******************************************************************************/ + public Integer getMaxInputRecords() + { + return (this.maxInputRecords); + } + + + + /******************************************************************************* + ** Setter for maxInputRecords + *******************************************************************************/ + public void setMaxInputRecords(Integer maxInputRecords) + { + this.maxInputRecords = maxInputRecords; + } + + + + /******************************************************************************* + ** Fluent setter for maxInputRecords + *******************************************************************************/ + public QProcessMetaData withMaxInputRecords(Integer maxInputRecords) + { + this.maxInputRecords = maxInputRecords; + return (this); + } + } 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 c60e01b3..5a478053 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 @@ -22,7 +22,7 @@ package com.kingsrook.qqq.backend.core.model.metadata.processes; -import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.instances.QInstanceEnricher; /******************************************************************************* @@ -69,7 +69,7 @@ public abstract class QSupplementalProcessMetaData /******************************************************************************* ** *******************************************************************************/ - public void enrich(QProcessMetaData process) + public void enrich(QInstanceEnricher qInstanceEnricher, QProcessMetaData process) { //////////////////////// // noop in base class // diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/CouldNotFindQueryFilterForExtractStepException.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/CouldNotFindQueryFilterForExtractStepException.java new file mode 100644 index 00000000..2dea8212 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/CouldNotFindQueryFilterForExtractStepException.java @@ -0,0 +1,19 @@ +package com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend; + + +import com.kingsrook.qqq.backend.core.exceptions.QException; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class CouldNotFindQueryFilterForExtractStepException extends QException +{ + /******************************************************************************* + ** + *******************************************************************************/ + public CouldNotFindQueryFilterForExtractStepException(String message) + { + super(message); + } +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/ExtractViaQueryStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/ExtractViaQueryStep.java index c4d7ff89..1f3e776d 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/ExtractViaQueryStep.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/ExtractViaQueryStep.java @@ -223,7 +223,7 @@ public class ExtractViaQueryStep extends AbstractExtractStep return (new QQueryFilter().withCriteria(new QFilterCriteria(table.getPrimaryKeyField(), QCriteriaOperator.IN, idStrings))); } - throw (new QException("Could not find query filter for Extract step.")); + throw (new CouldNotFindQueryFilterForExtractStepException("Could not find query filter for Extract step.")); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLWithFrontendProcess.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLWithFrontendProcess.java index 126735ef..46138ec9 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLWithFrontendProcess.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLWithFrontendProcess.java @@ -407,6 +407,30 @@ public class StreamedETLWithFrontendProcess + /******************************************************************************* + ** Fluent setter for minInputRecords + ** + *******************************************************************************/ + public Builder withMinInputRecords(Integer minInputRecords) + { + processMetaData.setMinInputRecords(minInputRecords); + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for maxInputRecords + ** + *******************************************************************************/ + public Builder withMaxInputRecords(Integer maxInputRecords) + { + processMetaData.setMaxInputRecords(maxInputRecords); + return (this); + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ObjectUtils.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ObjectUtils.java index 3c16e64f..80901c44 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ObjectUtils.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ObjectUtils.java @@ -22,6 +22,9 @@ package com.kingsrook.qqq.backend.core.utils; +import java.util.function.Consumer; +import java.util.function.Predicate; +import com.kingsrook.qqq.backend.core.utils.lambdas.UnsafeConsumer; import com.kingsrook.qqq.backend.core.utils.lambdas.UnsafeSupplier; @@ -96,4 +99,44 @@ public class ObjectUtils return (defaultIfThrew); } + + + /******************************************************************************* + ** + *******************************************************************************/ + public static void ifNotNull(T object, Consumer consumer) + { + if(object != null) + { + consumer.accept(object); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static void ifNotNullUnsafe(T object, UnsafeConsumer consumer) throws E + { + if(object != null) + { + consumer.run(object); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static T requireConditionElse(T a, Predicate condition, T b) + { + if(condition.test(a)) + { + return (a); + } + return (b); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/collections/MapBuilder.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/collections/MapBuilder.java index 0a94ddfd..e03d4283 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/collections/MapBuilder.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/collections/MapBuilder.java @@ -35,18 +35,18 @@ import java.util.function.Supplier; ** ** Can use it 2 ways: ** MapBuilder.of(key, value, key2, value2, ...) => Map (a HashMap) - ** MapBuilder.of(SomeMap::new).with(key, value).with(key2, value2)...build() => SomeMap (the type you specify) + ** MapBuilder.of(() -> new SomeMap()).with(key, value).with(key2, value2)...build() => SomeMap (the type you specify) *******************************************************************************/ -public class MapBuilder +public class MapBuilder> { - private Map map; + private M map; /******************************************************************************* ** *******************************************************************************/ - private MapBuilder(Map map) + private MapBuilder(M map) { this.map = map; } @@ -56,7 +56,7 @@ public class MapBuilder /******************************************************************************* ** *******************************************************************************/ - public static MapBuilder of(Supplier> mapSupplier) + public static > MapBuilder of(Supplier mapSupplier) { return (new MapBuilder<>(mapSupplier.get())); } @@ -66,7 +66,7 @@ public class MapBuilder /******************************************************************************* ** *******************************************************************************/ - public MapBuilder with(K key, V value) + public MapBuilder with(K key, V value) { map.put(key, value); return (this); @@ -77,7 +77,7 @@ public class MapBuilder /******************************************************************************* ** *******************************************************************************/ - public Map build() + public M build() { return (this.map); } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/collections/MapBuilderTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/collections/MapBuilderTest.java index b4b17600..5bafa2bc 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/collections/MapBuilderTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/collections/MapBuilderTest.java @@ -78,7 +78,7 @@ class MapBuilderTest @Test void testTypeYouRequest() { - Map myTreeMap = MapBuilder.of(TreeMap::new).with("1", 1).with("2", 2).build(); + TreeMap myTreeMap = MapBuilder.of(() -> new TreeMap()).with("1", 1).with("2", 2).build(); assertTrue(myTreeMap instanceof TreeMap); } 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 7c94f10f..8a2f7dcd 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 @@ -25,6 +25,7 @@ package com.kingsrook.qqq.api.actions; import java.io.Serializable; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; @@ -33,12 +34,15 @@ import java.util.Set; import java.util.UUID; import com.kingsrook.qqq.api.javalin.QBadRequestException; import com.kingsrook.qqq.api.model.APIVersion; -import com.kingsrook.qqq.api.model.APIVersionRange; +import com.kingsrook.qqq.api.model.actions.HttpApiResponse; import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaData; import com.kingsrook.qqq.api.model.metadata.ApiOperation; import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessCustomizers; +import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessInput; +import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessInputFieldsContainer; import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessMetaData; -import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessMetaDataContainer; +import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessOutputInterface; +import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessUtils; import com.kingsrook.qqq.api.model.metadata.processes.PostRunApiProcessCustomizer; import com.kingsrook.qqq.api.model.metadata.processes.PreRunApiProcessCustomizer; import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaData; @@ -46,6 +50,7 @@ import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaDataContainer; import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader; import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper; import com.kingsrook.qqq.backend.core.actions.permissions.TablePermissionSubType; +import com.kingsrook.qqq.backend.core.actions.processes.QProcessCallback; import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction; import com.kingsrook.qqq.backend.core.actions.tables.CountAction; import com.kingsrook.qqq.backend.core.actions.tables.DeleteAction; @@ -89,6 +94,7 @@ import com.kingsrook.qqq.backend.core.model.statusmessages.PermissionDeniedMessa import com.kingsrook.qqq.backend.core.model.statusmessages.QErrorMessage; import com.kingsrook.qqq.backend.core.model.statusmessages.QStatusMessage; import com.kingsrook.qqq.backend.core.model.statusmessages.QWarningMessage; +import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.CouldNotFindQueryFilterForExtractStepException; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.Pair; import com.kingsrook.qqq.backend.core.utils.StringUtils; @@ -100,6 +106,7 @@ import org.json.JSONArray; import org.json.JSONObject; import org.json.JSONTokener; import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; +import static com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator.IN; /******************************************************************************* @@ -113,7 +120,6 @@ public class ApiImplementation // key: Pair, value: Map metaData> // /////////////////////////////////////////////////////////////////// private static Map, Map> tableApiNameMap = new HashMap<>(); - private static Map, Map> processApiNameMap = new HashMap<>(); @@ -913,14 +919,15 @@ public class ApiImplementation /******************************************************************************* ** *******************************************************************************/ - public static Map runProcess(ApiInstanceMetaData apiInstanceMetaData, String version, String processApiName, Map paramMap) throws QException + public static HttpApiResponse runProcess(ApiInstanceMetaData apiInstanceMetaData, String version, String processApiName, Map paramMap) throws QException { - QProcessMetaData process = validateProcessAndVersion(apiInstanceMetaData, version, processApiName); - String processName = process.getName(); - ApiProcessMetaData apiProcessMetaData = getApiProcessMetaDataIfProcessIsInApi(apiInstanceMetaData, process); + Pair pair = ApiProcessUtils.getProcessMetaDataPair(apiInstanceMetaData, version, processApiName); - List badRequestMessages = new ArrayList<>(); - Map output = new LinkedHashMap<>(); + ApiProcessMetaData apiProcessMetaData = pair.getA(); + QProcessMetaData process = pair.getB(); + String processName = process.getName(); + + List badRequestMessages = new ArrayList<>(); String processUUID = UUID.randomUUID().toString(); @@ -928,27 +935,37 @@ public class ApiImplementation runProcessInput.setProcessName(processName); runProcessInput.setFrontendStepBehavior(RunProcessInput.FrontendStepBehavior.SKIP); runProcessInput.setProcessUUID(processUUID); - // todo i don't think runProcessInput.setCallback(); // todo i don't think runProcessInput.setAsyncJobCallback(); ////////////////////// // map input values // ////////////////////// - for(QFieldMetaData inputField : CollectionUtils.nonNullList(apiProcessMetaData.getInputFields())) + ApiProcessInput apiProcessInput = apiProcessMetaData.getInput(); + if(apiProcessInput != null) { - String value = paramMap.get(inputField.getName()); - if(!StringUtils.hasContent(value) && inputField.getIsRequired()) - { - badRequestMessages.add("Missing value for required input field " + inputField.getName()); - continue; - } - - // todo - types? - - runProcessInput.addValue(inputField.getName(), value); + processProcessInputFields(paramMap, badRequestMessages, runProcessInput, apiProcessInput.getQueryStringParams()); + processProcessInputFields(paramMap, badRequestMessages, runProcessInput, apiProcessInput.getFormParams()); + processProcessInputFields(paramMap, badRequestMessages, runProcessInput, apiProcessInput.getObjectBodyParams()); } - // todo! runProcessInput.setRecords(records); + //////////////////////////////////////// + // get records for process, if needed // + //////////////////////////////////////// + if(process.getMinInputRecords() != null && process.getMinInputRecords() > 0) + { + if(apiProcessInput != null && apiProcessInput.getRecordIdsParamName() != null) + { + String idParam = apiProcessInput.getRecordIdsParamName(); + if(StringUtils.hasContent(idParam) && StringUtils.hasContent(paramMap.get(idParam))) + { + String[] ids = paramMap.get(idParam).split(","); + + QTableMetaData table = QContext.getQInstance().getTable(process.getTableName()); + QQueryFilter filter = new QQueryFilter(new QFilterCriteria(table.getPrimaryKeyField(), IN, Arrays.asList(ids))); + runProcessInput.setCallback(getCallback(filter)); + } + } + } ///////////////////////////////////////// // throw if bad inputs have been noted // @@ -978,8 +995,17 @@ public class ApiImplementation ///////////////////// // run the process // ///////////////////// - RunProcessAction runProcessAction = new RunProcessAction(); - RunProcessOutput runProcessOutput = runProcessAction.execute(runProcessInput); + RunProcessOutput runProcessOutput; + + try + { + RunProcessAction runProcessAction = new RunProcessAction(); + runProcessOutput = runProcessAction.execute(runProcessInput); + } + catch(CouldNotFindQueryFilterForExtractStepException e) + { + throw (new QBadRequestException("Records to run through this process were not specified.")); + } ///////////////////////////////////////// // run post-customizer, if there is one // @@ -993,12 +1019,42 @@ public class ApiImplementation /////////////////////// // map output values // /////////////////////// - for(QFieldMetaData outputField : apiProcessMetaData.getOutputFields()) + ApiProcessOutputInterface output = apiProcessMetaData.getOutput(); + if(output != null) { - output.put(outputField.getName(), runProcessOutput.getValues().get(outputField.getName())); + return (new HttpApiResponse(output.getSuccessStatusCode(runProcessInput, runProcessOutput), output.getOutputForProcess(runProcessInput, runProcessOutput))); + } + else + { + return (new HttpApiResponse(HttpStatus.Code.NO_CONTENT, "")); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static void processProcessInputFields(Map paramMap, List badRequestMessages, RunProcessInput runProcessInput, ApiProcessInputFieldsContainer fieldsContainer) + { + if(fieldsContainer == null) + { + return; } - return (output); + for(QFieldMetaData inputField : CollectionUtils.nonNullList(fieldsContainer.getFields())) + { + String value = paramMap.get(inputField.getName()); + if(!StringUtils.hasContent(value) && inputField.getIsRequired()) + { + badRequestMessages.add("Missing value for required input field " + inputField.getName()); + continue; + } + + // todo - types? + + runProcessInput.addValue(inputField.getName(), value); + } } @@ -1233,65 +1289,6 @@ public class ApiImplementation - /******************************************************************************* - ** - *******************************************************************************/ - public static QProcessMetaData validateProcessAndVersion(ApiInstanceMetaData apiInstanceMetaData, String version, String processApiName) throws QNotFoundException - { - QProcessMetaData process = getProcessByApiName(apiInstanceMetaData.getName(), version, processApiName); - LogPair[] logPairs = new LogPair[] { logPair("apiName", apiInstanceMetaData.getName()), logPair("version", version), logPair("processApiName", processApiName) }; - - if(process == null) - { - LOG.info("404 because process is null (processApiName=" + processApiName + ")", logPairs); - throw (new QNotFoundException("Could not find a process named " + processApiName + " in this api.")); - } - - if(BooleanUtils.isTrue(process.getIsHidden())) - { - LOG.info("404 because process isHidden", logPairs); - throw (new QNotFoundException("Could not find a process named " + processApiName + " in this api.")); - } - - ApiProcessMetaDataContainer apiProcessMetaDataContainer = ApiProcessMetaDataContainer.of(process); - if(apiProcessMetaDataContainer == null) - { - LOG.info("404 because process apiProcessMetaDataContainer is null", logPairs); - throw (new QNotFoundException("Could not find a process named " + processApiName + " in this api.")); - } - - ApiProcessMetaData apiProcessMetaData = apiProcessMetaDataContainer.getApiProcessMetaData(apiInstanceMetaData.getName()); - if(apiProcessMetaData == null) - { - LOG.info("404 because process apiProcessMetaData is null", logPairs); - throw (new QNotFoundException("Could not find a process named " + processApiName + " in this api.")); - } - - if(BooleanUtils.isTrue(apiProcessMetaData.getIsExcluded())) - { - LOG.info("404 because process is excluded", logPairs); - throw (new QNotFoundException("Could not find a process named " + processApiName + " in this api.")); - } - - APIVersion requestApiVersion = new APIVersion(version); - List supportedVersions = apiInstanceMetaData.getSupportedVersions(); - if(CollectionUtils.nullSafeIsEmpty(supportedVersions) || !supportedVersions.contains(requestApiVersion)) - { - LOG.info("404 because requested version is not supported", logPairs); - throw (new QNotFoundException(version + " is not a supported version in this api.")); - } - - if(!apiProcessMetaData.getApiVersionRange().includes(requestApiVersion)) - { - LOG.info("404 because process version range does not include requested version", logPairs); - throw (new QNotFoundException(version + " is not a supported version for process " + processApiName + " in this api.")); - } - - return (process); - } - - - /******************************************************************************* ** *******************************************************************************/ @@ -1333,99 +1330,6 @@ public class ApiImplementation - /******************************************************************************* - ** - *******************************************************************************/ - private static QProcessMetaData getProcessByApiName(String apiName, String version, String processApiName) - { - ///////////////////////////////////////////////////////////////////////////////////////////// - // processApiNameMap is a map of (apiName,apiVersion) => Map. // - // that is to say, a 2-level map. The first level is keyed by (apiName,apiVersion) pairs. // - // the second level is keyed by processApiNames. // - ///////////////////////////////////////////////////////////////////////////////////////////// - Pair key = new Pair<>(apiName, version); - if(processApiNameMap.get(key) == null) - { - Map map = new HashMap<>(); - - for(QProcessMetaData process : QContext.getQInstance().getProcesses().values()) - { - ApiProcessMetaDataContainer apiProcessMetaDataContainer = ApiProcessMetaDataContainer.of(process); - if(apiProcessMetaDataContainer != null) - { - ApiProcessMetaData apiProcessMetaData = apiProcessMetaDataContainer.getApiProcessMetaData(apiName); - if(apiProcessMetaData != null) - { - String name = process.getName(); - if(StringUtils.hasContent(apiProcessMetaData.getApiProcessName())) - { - name = apiProcessMetaData.getApiProcessName(); - } - map.put(name, process); - } - } - } - - processApiNameMap.put(key, map); - } - - return (processApiNameMap.get(key).get(processApiName)); - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - public static ApiProcessMetaData getApiProcessMetaDataIfProcessIsInApi(ApiInstanceMetaData apiInstanceMetaData, QProcessMetaData process) - { - if(BooleanUtils.isTrue(process.getIsHidden())) - { - LOG.trace("excluding process because it is hidden (process=" + process.getName() + ")"); - return (null); - } - - ApiProcessMetaDataContainer apiProcessMetaDataContainer = ApiProcessMetaDataContainer.of(process); - if(apiProcessMetaDataContainer == null) - { - LOG.trace("excluding process because apiProcessMetaDataContainer is null (process=" + process.getName() + ")"); - return (null); - } - - ApiProcessMetaData apiProcessMetaData = apiProcessMetaDataContainer.getApiProcessMetaData(apiInstanceMetaData.getName()); - if(apiProcessMetaData == null) - { - LOG.trace("excluding process because apiProcessMetaData is null (process=" + process.getName() + ")"); - return (null); - } - - if(BooleanUtils.isTrue(apiProcessMetaData.getIsExcluded())) - { - LOG.trace("excluding process because is excluded (process=" + process.getName() + ")"); - return (null); - } - - boolean isProcessInAnySupportedVersions = false; - List supportedVersions = apiInstanceMetaData.getSupportedVersions(); - APIVersionRange apiVersionRange = apiProcessMetaData.getApiVersionRange(); - for(APIVersion supportedVersion : supportedVersions) - { - if(apiVersionRange.includes(supportedVersion)) - { - isProcessInAnySupportedVersions = true; - } - } - - if(!isProcessInAnySupportedVersions) - { - LOG.trace("excluding process because it is not in any supported versions (process=" + process.getName() + ")"); - return (null); - } - - return (apiProcessMetaData); - } - - /******************************************************************************* ** @@ -1445,4 +1349,29 @@ public class ApiImplementation return errors.stream().anyMatch(e -> (e instanceof NotFoundStatusMessage)); } + + + /******************************************************************************* + ** + *******************************************************************************/ + private static QProcessCallback getCallback(QQueryFilter filter) + { + return new QProcessCallback() + { + @Override + public QQueryFilter getQueryFilter() + { + return (filter); + } + + + + @Override + public Map getFieldValues(List fields) + { + return (Collections.emptyMap()); + } + }; + } + } 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 1aa18b5c..a37efeac 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,6 +41,11 @@ import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaData; import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaDataContainer; import com.kingsrook.qqq.api.model.metadata.ApiOperation; import com.kingsrook.qqq.api.model.metadata.fields.ApiFieldMetaData; +import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessInput; +import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessInputFieldsContainer; +import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessMetaData; +import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessMetaDataContainer; +import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessUtils; import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaData; import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaDataContainer; import com.kingsrook.qqq.api.model.openapi.Components; @@ -73,12 +78,14 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValue; import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource; import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.Association; import com.kingsrook.qqq.backend.core.model.metadata.tables.Capability; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.JsonUtils; import com.kingsrook.qqq.backend.core.utils.ObjectUtils; +import com.kingsrook.qqq.backend.core.utils.Pair; import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.core.utils.YamlUtils; import com.kingsrook.qqq.backend.core.utils.collections.ListBuilder; @@ -192,8 +199,9 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction tables = new ArrayList<>(qInstance.getTables().values()); + 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) { @@ -330,7 +339,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction> apiProcessMetaDataList = getProcessesUnderTable(table, apiName, apiVersion); + + if(!getEnabled && !queryByQueryStringEnabled && !insertEnabled && !insertBulkEnabled && !updateEnabled && !updateBulkEnabled && !deleteEnabled && !deleteBulkEnabled && !CollectionUtils.nullSafeHasContents(apiProcessMetaDataList)) { - LOG.debug("Omitting table [" + tableName + "] because it does not have any supported capabilities / enabled operations"); + LOG.debug("Omitting table [" + tableName + "] because it does not have any supported capabilities / enabled operations or processes"); continue; } @@ -687,6 +698,35 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction pair : CollectionUtils.nonNullList(apiProcessMetaDataList)) + { + ApiProcessMetaData apiProcessMetaData = pair.getA(); + QProcessMetaData processMetaData = pair.getB(); + + String processApiPath = ApiProcessUtils.getProcessApiPath(qInstance, processMetaData, apiProcessMetaData, apiInstanceMetaData); + Path path = generateProcessSpecPathObject(apiInstanceMetaData, apiProcessMetaData, processMetaData, ListBuilder.of(tableLabel)); + openAPI.getPaths().put(basePath + processApiPath, path); + + usedProcessNames.add(processMetaData.getName()); + } + } + + ///////////////////////////// + // add non-table processes // + ///////////////////////////// + List> 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); + + usedProcessNames.add(processMetaData.getName()); } componentResponses.put("error" + HttpStatus.BAD_REQUEST.getCode(), buildStandardErrorResponse("Bad Request. Some portion of the request's content was not acceptable to the server. See error message in body for details.", "Parameter id should be given an integer value, but received string: \"Foo\"")); @@ -710,6 +750,140 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction> getProcessesNotUnderTables(String apiName, APIVersion apiVersion, Set usedProcessNames) + { + List> apiProcessMetaDataList = new ArrayList<>(); + for(QProcessMetaData processMetaData : CollectionUtils.nonNullMap(QContext.getQInstance().getProcesses()).values()) + { + if(usedProcessNames.contains(processMetaData.getName())) + { + continue; + } + + ApiProcessMetaDataContainer apiProcessMetaDataContainer = ApiProcessMetaDataContainer.of(processMetaData); + if(apiProcessMetaDataContainer == null) + { + continue; + } + + ApiProcessMetaData apiProcessMetaData = apiProcessMetaDataContainer.getApis().get(apiName); + if(apiProcessMetaData == null) + { + continue; + } + + if(!apiProcessMetaData.getApiVersionRange().includes(apiVersion)) + { + continue; + } + + apiProcessMetaDataList.add(Pair.of(apiProcessMetaData, processMetaData)); + } + return (apiProcessMetaDataList); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private Path generateProcessSpecPathObject(ApiInstanceMetaData apiInstanceMetaData, ApiProcessMetaData apiProcessMetaData, QProcessMetaData processMetaData, List tags) + { + Method methodForProcess = new Method() + .withOperationId(apiProcessMetaData.getApiProcessName()) + .withTags(tags) + .withSummary(processMetaData.getLabel()) // todo - add optional summary to meta data + .withDescription("Run the process named " + processMetaData.getLabel())// todo - add optional description to meta data, .withDescription() + .withSecurity(getSecurity(apiInstanceMetaData, "todo - process name")); + + List parameters = new ArrayList<>(); + ApiProcessInput apiProcessInput = apiProcessMetaData.getInput(); + if(apiProcessInput != null) + { + ApiProcessInputFieldsContainer queryStringParams = apiProcessInput.getQueryStringParams(); + if(queryStringParams != null) + { + for(QFieldMetaData field : CollectionUtils.nonNullList(queryStringParams.getFields())) + { + parameters.add(new Parameter() + .withName(field.getName()) + // todo - add description to meta data .withDescription("Which page of results to return. Starts at 1.") + .withDescription("Value for the " + field.getLabel() + " field.") + .withIn("query") + .withRequired(field.getIsRequired()) + .withSchema(new Schema().withType(getFieldType(field)))); + } + } + } + + if(CollectionUtils.nullSafeHasContents(parameters)) + { + methodForProcess.setParameters(parameters); + } + + // todo methodForProcess.withRequestBody(); + + // todo methodForProcess.withResponse(); + + methodForProcess.withResponse(HttpStatus.OK.getCode(), new Response() + .withDescription("Successfully ran the process") + .withContent(MapBuilder.of("application/json", new Content()))); + + @SuppressWarnings("checkstyle:indentation") + Path path = switch(apiProcessMetaData.getMethod()) + { + case GET -> new Path().withGet(methodForProcess); + case POST -> new Path().withPost(methodForProcess); + case PUT -> new Path().withPut(methodForProcess); + case PATCH -> new Path().withPatch(methodForProcess); + case DELETE -> new Path().withDelete(methodForProcess); + }; + + return (path); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private List> getProcessesUnderTable(QTableMetaData table, String apiName, APIVersion apiVersion) + { + List> apiProcessMetaDataList = new ArrayList<>(); + for(QProcessMetaData processMetaData : CollectionUtils.nonNullMap(QContext.getQInstance().getProcesses()).values()) + { + if(!table.getName().equals(processMetaData.getTableName())) + { + continue; + } + + ApiProcessMetaDataContainer apiProcessMetaDataContainer = ApiProcessMetaDataContainer.of(processMetaData); + if(apiProcessMetaDataContainer == null) + { + continue; + } + + ApiProcessMetaData apiProcessMetaData = apiProcessMetaDataContainer.getApis().get(apiName); + if(apiProcessMetaData == null) + { + continue; + } + + if(!apiProcessMetaData.getApiVersionRange().includes(apiVersion)) + { + continue; + } + + apiProcessMetaDataList.add(Pair.of(apiProcessMetaData, processMetaData)); + } + return (apiProcessMetaDataList); + } + + + /******************************************************************************* ** written for the use-case of, generating a single table's api, but it has ** associations that it references, so we need their schemas too - so, make diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandler.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandler.java index 764884cc..12dd78aa 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandler.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandler.java @@ -28,11 +28,13 @@ import java.nio.charset.StandardCharsets; import java.time.Instant; import java.util.ArrayList; import java.util.Base64; +import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.function.BiFunction; import java.util.stream.Collectors; import com.fasterxml.jackson.annotation.JsonInclude; import com.kingsrook.qqq.api.actions.ApiImplementation; @@ -41,10 +43,15 @@ import com.kingsrook.qqq.api.model.APILog; import com.kingsrook.qqq.api.model.APIVersion; import com.kingsrook.qqq.api.model.actions.GenerateOpenApiSpecInput; import com.kingsrook.qqq.api.model.actions.GenerateOpenApiSpecOutput; +import com.kingsrook.qqq.api.model.actions.HttpApiResponse; import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaData; import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaDataContainer; import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaDataProvider; +import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessInput; +import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessInputFieldsContainer; import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessMetaData; +import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessMetaDataContainer; +import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessUtils; import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaData; import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaDataContainer; import com.kingsrook.qqq.api.model.openapi.HttpMethod; @@ -80,6 +87,7 @@ import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModu import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.ExceptionUtils; import com.kingsrook.qqq.backend.core.utils.JsonUtils; +import com.kingsrook.qqq.backend.core.utils.ObjectUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder; import com.kingsrook.qqq.backend.javalin.QJavalinAccessLogger; @@ -91,6 +99,7 @@ import io.javalin.http.Context; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.BooleanUtils; import org.eclipse.jetty.http.HttpStatus; +import org.json.JSONObject; import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; import static com.kingsrook.qqq.backend.javalin.QJavalinImplementation.SLOW_LOG_THRESHOLD_MS; @@ -102,6 +111,8 @@ public class QJavalinApiHandler { private static final QLogger LOG = QLogger.getLogger(QJavalinApiHandler.class); + private static final ApiProcessMetaDataContainer EMPTY_CONTAINER = new ApiProcessMetaDataContainer().withApis(Collections.emptyMap()); + private static QInstance qInstance; private static Map apiLogUserIdCache = new HashMap<>(); @@ -174,10 +185,11 @@ public class QJavalinApiHandler /////////////////// for(QProcessMetaData process : qInstance.getProcesses().values()) { - ApiProcessMetaData apiProcessMetaData = ApiImplementation.getApiProcessMetaDataIfProcessIsInApi(apiInstanceMetaData, process); + ApiProcessMetaDataContainer apiProcessMetaDataContainer = Objects.requireNonNullElse(ApiProcessMetaDataContainer.of(process), EMPTY_CONTAINER); + ApiProcessMetaData apiProcessMetaData = apiProcessMetaDataContainer.getApis().get(apiInstanceMetaData.getName()); if(apiProcessMetaData != null) { - String path = getProcessApiPath(process, apiProcessMetaData, apiInstanceMetaData); + String path = ApiProcessUtils.getProcessApiPath(qInstance, process, apiProcessMetaData, apiInstanceMetaData); HttpMethod method = apiProcessMetaData.getMethod(); switch(method) { @@ -189,6 +201,8 @@ public class QJavalinApiHandler default -> throw (new QRuntimeException("Unrecognized http method [" + method + "] for process [" + process.getName() + "]")); } + make405sForOtherMethods(method, path); + if(doesProcessSupportAsync(apiInstanceMetaData, process)) { ApiBuilder.get(path + "/status/{processId}", context -> getProcessStatus(context, apiInstanceMetaData)); @@ -247,6 +261,49 @@ public class QJavalinApiHandler + /******************************************************************************* + ** + *******************************************************************************/ + private void make405sForOtherMethods(HttpMethod allowedMethod, String path) + { + if(!allowedMethod.equals(HttpMethod.GET)) + { + ApiBuilder.get(path, (Context c) -> QJavalinApiHandler.return405(c, allowedMethod)); + } + + if(!allowedMethod.equals(HttpMethod.POST)) + { + ApiBuilder.post(path, (Context c) -> QJavalinApiHandler.return405(c, allowedMethod)); + } + + if(!allowedMethod.equals(HttpMethod.PUT)) + { + ApiBuilder.put(path, (Context c) -> QJavalinApiHandler.return405(c, allowedMethod)); + } + + if(!allowedMethod.equals(HttpMethod.PATCH)) + { + ApiBuilder.patch(path, (Context c) -> QJavalinApiHandler.return405(c, allowedMethod)); + } + + if(!allowedMethod.equals(HttpMethod.DELETE)) + { + ApiBuilder.delete(path, (Context c) -> QJavalinApiHandler.return405(c, allowedMethod)); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static void return405(Context context, HttpMethod allowedMethod) + { + respondWithError(context, HttpStatus.Code.METHOD_NOT_ALLOWED, "This path only supports method: " + allowedMethod, newAPILog(context)); // 405 + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -272,21 +329,26 @@ public class QJavalinApiHandler QJavalinAccessLogger.logStart("apiRunProcess", logPair("process", processMetaData.getName())); Map parameters = new LinkedHashMap<>(); - for(QFieldMetaData inputField : CollectionUtils.nonNullList(apiProcessMetaData.getInputFields())) + + ApiProcessInput input = apiProcessMetaData.getInput(); + if(input != null) { - String value = switch(apiProcessMetaData.getMethod()) + processProcessInputFieldsContainer(context, parameters, input.getQueryStringParams(), Context::queryParam); + processProcessInputFieldsContainer(context, parameters, input.getFormParams(), Context::formParam); + + ApiProcessInputFieldsContainer objectBodyParams = input.getObjectBodyParams(); + if(objectBodyParams != null) { - case GET -> context.queryParam(inputField.getName()); - // todo - other methods (all from a JSON body??) - default -> throw new QException("Http method " + apiLog.getMethod() + " is not yet implemented for reading parameters"); - }; - parameters.put(inputField.getName(), value); + JSONObject jsonObject = new JSONObject(context.body()); + processProcessInputFieldsContainer(context, parameters, objectBodyParams, (ctx, name) -> jsonObject.optString(name, null)); + } } - Map outputRecord = ApiImplementation.runProcess(apiInstanceMetaData, version, apiProcessMetaData.getApiProcessName(), parameters); + HttpApiResponse response = ApiImplementation.runProcess(apiInstanceMetaData, version, apiProcessMetaData.getApiProcessName(), parameters); + context.status(response.getStatusCode().getCode()); QJavalinAccessLogger.logEndSuccess(); - String resultString = toJson(outputRecord); + String resultString = toJson(Objects.requireNonNullElse(response.getResponseBodyObject(), "")); context.result(resultString); storeApiLog(apiLog.withStatusCode(context.statusCode()).withResponseBody(resultString)); } @@ -302,10 +364,22 @@ public class QJavalinApiHandler /******************************************************************************* ** *******************************************************************************/ - private boolean doesProcessSupportAsync(ApiInstanceMetaData apiInstanceMetaData, QProcessMetaData process) + private static void processProcessInputFieldsContainer(Context context, Map parameters, ApiProcessInputFieldsContainer fieldsContainer, BiFunction paramAccessor) { - // todo - implement - return false; + if(fieldsContainer != null) + { + List fields = CollectionUtils.nonNullList(fieldsContainer.getFields()); + ObjectUtils.ifNotNull(fieldsContainer.getRecordIdsField(), fields::add); + for(QFieldMetaData field : fields) + { + String queryParamValue = paramAccessor.apply(context, field.getName()); + if(queryParamValue != null) + { + String backendName = ObjectUtils.requireConditionElse(field.getBackendName(), StringUtils::hasContent, field.getName()); + parameters.put(backendName, queryParamValue); + } + } + } } @@ -313,34 +387,10 @@ public class QJavalinApiHandler /******************************************************************************* ** *******************************************************************************/ - private String getProcessApiPath(QProcessMetaData process, ApiProcessMetaData apiProcessMetaData, ApiInstanceMetaData apiInstanceMetaData) + private boolean doesProcessSupportAsync(ApiInstanceMetaData apiInstanceMetaData, QProcessMetaData process) { - if(StringUtils.hasContent(apiProcessMetaData.getPath())) - { - return apiProcessMetaData.getPath() + "/" + apiProcessMetaData.getApiProcessName(); - } - else if(StringUtils.hasContent(process.getTableName())) - { - QTableMetaData table = qInstance.getTable(process.getTableName()); - String tablePathPart = table.getName(); - ApiTableMetaDataContainer apiTableMetaDataContainer = ApiTableMetaDataContainer.of(table); - if(apiTableMetaDataContainer != null) - { - ApiTableMetaData apiTableMetaData = apiTableMetaDataContainer.getApis().get(apiInstanceMetaData.getName()); - if(apiTableMetaData != null) - { - if(StringUtils.hasContent(apiTableMetaData.getApiTableName())) - { - tablePathPart = apiTableMetaData.getApiTableName(); - } - } - } - return tablePathPart + "/" + apiProcessMetaData.getApiProcessName(); - } - else - { - return apiProcessMetaData.getApiProcessName(); - } + // todo - implement + return false; } diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/actions/HttpApiResponse.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/actions/HttpApiResponse.java new file mode 100644 index 00000000..5d3b1c66 --- /dev/null +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/actions/HttpApiResponse.java @@ -0,0 +1,122 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.api.model.actions; + + +import java.io.Serializable; +import org.eclipse.jetty.http.HttpStatus; + + +/******************************************************************************* + ** class to contain http api responses. + ** + *******************************************************************************/ +public class HttpApiResponse +{ + private HttpStatus.Code statusCode; + private Serializable responseBodyObject; + + + + /******************************************************************************* + ** Default Constructor + ** + *******************************************************************************/ + public HttpApiResponse() + { + } + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public HttpApiResponse(HttpStatus.Code statusCode, Serializable responseBodyObject) + { + this.statusCode = statusCode; + this.responseBodyObject = responseBodyObject; + } + + + + /******************************************************************************* + ** Getter for statusCode + *******************************************************************************/ + public HttpStatus.Code getStatusCode() + { + return (this.statusCode); + } + + + + /******************************************************************************* + ** Setter for statusCode + *******************************************************************************/ + public void setStatusCode(HttpStatus.Code statusCode) + { + this.statusCode = statusCode; + } + + + + /******************************************************************************* + ** Fluent setter for statusCode + *******************************************************************************/ + public HttpApiResponse withStatusCode(HttpStatus.Code statusCode) + { + this.statusCode = statusCode; + return (this); + } + + + + /******************************************************************************* + ** Getter for responseBodyObject + *******************************************************************************/ + public Serializable getResponseBodyObject() + { + return (this.responseBodyObject); + } + + + + /******************************************************************************* + ** Setter for responseBodyObject + *******************************************************************************/ + public void setResponseBodyObject(Serializable responseBodyObject) + { + this.responseBodyObject = responseBodyObject; + } + + + + /******************************************************************************* + ** Fluent setter for responseBodyObject + *******************************************************************************/ + public HttpApiResponse withResponseBodyObject(Serializable responseBodyObject) + { + this.responseBodyObject = responseBodyObject; + return (this); + } + +} \ No newline at end of file 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 new file mode 100644 index 00000000..f533b9ae --- /dev/null +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessInput.java @@ -0,0 +1,130 @@ +package com.kingsrook.qqq.api.model.metadata.processes; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class ApiProcessInput +{ + private ApiProcessInputFieldsContainer queryStringParams; + private ApiProcessInputFieldsContainer formParams; + private ApiProcessInputFieldsContainer recordBodyParams; + + + + /******************************************************************************* + ** + *******************************************************************************/ + public String getRecordIdsParamName() + { + if(queryStringParams != null && queryStringParams.getRecordIdsField() != null) + { + return (queryStringParams.getRecordIdsField().getName()); + } + + if(formParams != null && formParams.getRecordIdsField() != null) + { + return (formParams.getRecordIdsField().getName()); + } + + if(recordBodyParams != null && recordBodyParams.getRecordIdsField() != null) + { + return (recordBodyParams.getRecordIdsField().getName()); + } + + return (null); + } + + + + /******************************************************************************* + ** Getter for queryStringParams + *******************************************************************************/ + public ApiProcessInputFieldsContainer getQueryStringParams() + { + return (this.queryStringParams); + } + + + + /******************************************************************************* + ** Setter for queryStringParams + *******************************************************************************/ + public void setQueryStringParams(ApiProcessInputFieldsContainer queryStringParams) + { + this.queryStringParams = queryStringParams; + } + + + + /******************************************************************************* + ** Fluent setter for queryStringParams + *******************************************************************************/ + public ApiProcessInput withQueryStringParams(ApiProcessInputFieldsContainer queryStringParams) + { + this.queryStringParams = queryStringParams; + return (this); + } + + + + /******************************************************************************* + ** Getter for formParams + *******************************************************************************/ + public ApiProcessInputFieldsContainer getFormParams() + { + return (this.formParams); + } + + + + /******************************************************************************* + ** Setter for formParams + *******************************************************************************/ + public void setFormParams(ApiProcessInputFieldsContainer formParams) + { + this.formParams = formParams; + } + + + + /******************************************************************************* + ** Fluent setter for formParams + *******************************************************************************/ + public ApiProcessInput withFormParams(ApiProcessInputFieldsContainer formParams) + { + this.formParams = formParams; + return (this); + } + + + + /******************************************************************************* + ** Getter for recordBodyParams + *******************************************************************************/ + public ApiProcessInputFieldsContainer getObjectBodyParams() + { + return (this.recordBodyParams); + } + + + + /******************************************************************************* + ** Setter for recordBodyParams + *******************************************************************************/ + public void setRecordBodyParams(ApiProcessInputFieldsContainer recordBodyParams) + { + this.recordBodyParams = recordBodyParams; + } + + + + /******************************************************************************* + ** Fluent setter for recordBodyParams + *******************************************************************************/ + public ApiProcessInput withRecordBodyParams(ApiProcessInputFieldsContainer recordBodyParams) + { + this.recordBodyParams = recordBodyParams; + return (this); + } +} 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 new file mode 100644 index 00000000..7c5b0060 --- /dev/null +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessInputFieldsContainer.java @@ -0,0 +1,116 @@ +package com.kingsrook.qqq.api.model.metadata.processes; + + +import java.util.ArrayList; +import java.util.List; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class ApiProcessInputFieldsContainer +{ + private QFieldMetaData recordIdsField; + private List fields; + + + + /******************************************************************************* + ** + *******************************************************************************/ + public ApiProcessInputFieldsContainer withInferredInputFields(QProcessMetaData processMetaData) + { + fields = new ArrayList<>(); + for(QStepMetaData stepMetaData : CollectionUtils.nonNullList(processMetaData.getStepList())) + { + if(stepMetaData instanceof QFrontendStepMetaData frontendStep) + { + fields.addAll(frontendStep.getInputFields()); + } + } + + return (this); + } + + + + /******************************************************************************* + ** Getter for recordIdsField + *******************************************************************************/ + public QFieldMetaData getRecordIdsField() + { + return (this.recordIdsField); + } + + + + /******************************************************************************* + ** Setter for recordIdsField + *******************************************************************************/ + public void setRecordIdsField(QFieldMetaData recordIdsField) + { + this.recordIdsField = recordIdsField; + } + + + + /******************************************************************************* + ** Fluent setter for recordIdsField + *******************************************************************************/ + public ApiProcessInputFieldsContainer withRecordIdsField(QFieldMetaData recordIdsField) + { + this.recordIdsField = recordIdsField; + return (this); + } + + + + /******************************************************************************* + ** Getter for fields + *******************************************************************************/ + public List getFields() + { + return (this.fields); + } + + + + /******************************************************************************* + ** Setter for fields + *******************************************************************************/ + public void setFields(List fields) + { + this.fields = fields; + } + + + + /******************************************************************************* + ** Fluent setter for fields + *******************************************************************************/ + public ApiProcessInputFieldsContainer withField(QFieldMetaData field) + { + if(this.fields == null) + { + this.fields = new ArrayList<>(); + } + this.fields.add(field); + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for fields + *******************************************************************************/ + public ApiProcessInputFieldsContainer withFields(List fields) + { + this.fields = fields; + return (this); + } +} 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 d3920a95..b3cfe92a 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 @@ -22,7 +22,6 @@ package com.kingsrook.qqq.api.model.metadata.processes; -import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -31,13 +30,13 @@ import com.kingsrook.qqq.api.model.APIVersionRange; import com.kingsrook.qqq.api.model.metadata.fields.ApiFieldMetaData; import com.kingsrook.qqq.api.model.metadata.fields.ApiFieldMetaDataContainer; import com.kingsrook.qqq.api.model.openapi.HttpMethod; +import com.kingsrook.qqq.backend.core.instances.QInstanceEnricher; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; -import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; -import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils; +import com.kingsrook.qqq.backend.core.utils.collections.ListBuilder; /******************************************************************************* @@ -54,51 +53,13 @@ public class ApiProcessMetaData private String path; private HttpMethod method; - private List inputFields; - private List outputFields; + private ApiProcessInput input; + private ApiProcessOutputInterface output; private Map customizers; - /******************************************************************************* - ** - *******************************************************************************/ - public ApiProcessMetaData withInferredInputFields(QProcessMetaData processMetaData) - { - inputFields = new ArrayList<>(); - for(QStepMetaData stepMetaData : CollectionUtils.nonNullList(processMetaData.getStepList())) - { - if(stepMetaData instanceof QFrontendStepMetaData frontendStep) - { - inputFields.addAll(frontendStep.getInputFields()); - } - } - - return (this); - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - public ApiProcessMetaData withInferredOutputFields(QProcessMetaData processMetaData) - { - outputFields = new ArrayList<>(); - for(QStepMetaData stepMetaData : CollectionUtils.nonNullList(processMetaData.getStepList())) - { - if(stepMetaData instanceof QFrontendStepMetaData frontendStep) - { - outputFields.addAll(frontendStep.getOutputFields()); - } - } - - return (this); - } - - - /******************************************************************************* ** *******************************************************************************/ @@ -119,8 +80,7 @@ public class ApiProcessMetaData /******************************************************************************* ** *******************************************************************************/ - @SuppressWarnings("unchecked") - public void enrich(String apiName, QProcessMetaData process) + public void enrich(QInstanceEnricher qInstanceEnricher, String apiName, QProcessMetaData process) { if(!StringUtils.hasContent(getApiProcessName())) { @@ -129,15 +89,19 @@ public class ApiProcessMetaData if(initialVersion != null) { - /////////////////////////////////////////////////////////////// - // make sure all fields have at least an initial version set // - /////////////////////////////////////////////////////////////// - for(QFieldMetaData field : CollectionUtils.mergeLists(getInputFields(), getOutputFields())) + if(getOutput() instanceof ApiProcessObjectOutput outputObject) { - ApiFieldMetaData apiFieldMetaData = ensureFieldHasApiSupplementalMetaData(apiName, field); - if(apiFieldMetaData.getInitialVersion() == null) + enrichFieldList(qInstanceEnricher, apiName, outputObject.getOutputFields()); + } + + if(input != null) + { + for(ApiProcessInputFieldsContainer fieldsContainer : ListBuilder.of(input.getQueryStringParams(), input.getFormParams(), input.getObjectBodyParams())) { - apiFieldMetaData.setInitialVersion(initialVersion); + if(fieldsContainer != null) + { + enrichFieldList(qInstanceEnricher, apiName, fieldsContainer.getFields()); + } } } } @@ -145,6 +109,25 @@ public class ApiProcessMetaData + /******************************************************************************* + ** + *******************************************************************************/ + private void enrichFieldList(QInstanceEnricher qInstanceEnricher, String apiName, List fields) + { + for(QFieldMetaData field : CollectionUtils.nonNullList(fields)) + { + ApiFieldMetaData apiFieldMetaData = ensureFieldHasApiSupplementalMetaData(apiName, field); + if(apiFieldMetaData.getInitialVersion() == null) + { + apiFieldMetaData.setInitialVersion(initialVersion); + } + + qInstanceEnricher.enrichField(field); + } + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -166,36 +149,6 @@ public class ApiProcessMetaData - /******************************************************************************* - ** Fluent setter for a single outputField - *******************************************************************************/ - public ApiProcessMetaData withOutputField(QFieldMetaData outputField) - { - if(this.outputFields == null) - { - this.outputFields = new ArrayList<>(); - } - this.outputFields.add(outputField); - return (this); - } - - - - /******************************************************************************* - ** Fluent setter for a single inputField - *******************************************************************************/ - public ApiProcessMetaData withInputField(QFieldMetaData inputField) - { - if(this.inputFields == null) - { - this.inputFields = new ArrayList<>(); - } - this.inputFields.add(inputField); - return (this); - } - - - /******************************************************************************* ** Getter for initialVersion *******************************************************************************/ @@ -382,68 +335,6 @@ public class ApiProcessMetaData - /******************************************************************************* - ** Getter for inputFields - *******************************************************************************/ - public List getInputFields() - { - return (this.inputFields); - } - - - - /******************************************************************************* - ** Setter for inputFields - *******************************************************************************/ - public void setInputFields(List inputFields) - { - this.inputFields = inputFields; - } - - - - /******************************************************************************* - ** Fluent setter for inputFields - *******************************************************************************/ - public ApiProcessMetaData withInputFields(List inputFields) - { - this.inputFields = inputFields; - return (this); - } - - - - /******************************************************************************* - ** Getter for outputFields - *******************************************************************************/ - public List getOutputFields() - { - return (this.outputFields); - } - - - - /******************************************************************************* - ** Setter for outputFields - *******************************************************************************/ - public void setOutputFields(List outputFields) - { - this.outputFields = outputFields; - } - - - - /******************************************************************************* - ** Fluent setter for outputFields - *******************************************************************************/ - public ApiProcessMetaData withOutputFields(List outputFields) - { - this.outputFields = outputFields; - return (this); - } - - - /******************************************************************************* ** Getter for customizers *******************************************************************************/ @@ -493,4 +384,66 @@ public class ApiProcessMetaData return (this); } + + + /******************************************************************************* + ** Getter for output + *******************************************************************************/ + public ApiProcessOutputInterface getOutput() + { + return (this.output); + } + + + + /******************************************************************************* + ** Setter for output + *******************************************************************************/ + public void setOutput(ApiProcessOutputInterface output) + { + this.output = output; + } + + + + /******************************************************************************* + ** Fluent setter for output + *******************************************************************************/ + public ApiProcessMetaData withOutput(ApiProcessOutputInterface output) + { + this.output = output; + return (this); + } + + + + /******************************************************************************* + ** Getter for input + *******************************************************************************/ + public ApiProcessInput getInput() + { + return (this.input); + } + + + + /******************************************************************************* + ** Setter for input + *******************************************************************************/ + public void setInput(ApiProcessInput input) + { + this.input = input; + } + + + + /******************************************************************************* + ** Fluent setter for input + *******************************************************************************/ + public ApiProcessMetaData withInput(ApiProcessInput input) + { + this.input = input; + return (this); + } + } 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 36a2b352..5fe3351a 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 @@ -25,6 +25,7 @@ package com.kingsrook.qqq.api.model.metadata.processes; import java.util.LinkedHashMap; import java.util.Map; import com.kingsrook.qqq.api.ApiSupplementType; +import com.kingsrook.qqq.backend.core.instances.QInstanceEnricher; import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QSupplementalProcessMetaData; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; @@ -64,13 +65,13 @@ public class ApiProcessMetaDataContainer extends QSupplementalProcessMetaData ** *******************************************************************************/ @Override - public void enrich(QProcessMetaData process) + public void enrich(QInstanceEnricher qInstanceEnricher, QProcessMetaData process) { - super.enrich(process); + super.enrich(qInstanceEnricher, process); for(Map.Entry entry : CollectionUtils.nonNullMap(apis).entrySet()) { - entry.getValue().enrich(entry.getKey(), process); + entry.getValue().enrich(qInstanceEnricher, entry.getKey(), process); } } 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 new file mode 100644 index 00000000..e92556a6 --- /dev/null +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessObjectOutput.java @@ -0,0 +1,85 @@ +package com.kingsrook.qqq.api.model.metadata.processes; + + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class ApiProcessObjectOutput implements ApiProcessOutputInterface +{ + private List outputFields; + + + + /******************************************************************************* + ** + ******************************************************************************/ + @Override + public Serializable getOutputForProcess(RunProcessInput runProcessInput, RunProcessOutput runProcessOutput) + { + LinkedHashMap outputMap = new LinkedHashMap<>(); + + for(QFieldMetaData outputField : CollectionUtils.nonNullList(getOutputFields())) + { + outputMap.put(outputField.getName(), runProcessOutput.getValues().get(outputField.getName())); + } + + return (outputMap); + } + + + + /******************************************************************************* + ** Getter for outputFields + *******************************************************************************/ + public List getOutputFields() + { + return (this.outputFields); + } + + + + /******************************************************************************* + ** Setter for outputFields + *******************************************************************************/ + public void setOutputFields(List outputFields) + { + this.outputFields = outputFields; + } + + + + /******************************************************************************* + ** Fluent setter for outputFields + *******************************************************************************/ + public ApiProcessObjectOutput withOutputFields(List outputFields) + { + this.outputFields = outputFields; + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for a single outputField + *******************************************************************************/ + public ApiProcessObjectOutput withOutputField(QFieldMetaData outputField) + { + if(this.outputFields == null) + { + this.outputFields = new ArrayList<>(); + } + this.outputFields.add(outputField); + return (this); + } + +} 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 new file mode 100644 index 00000000..476bb072 --- /dev/null +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessOutputInterface.java @@ -0,0 +1,30 @@ +package com.kingsrook.qqq.api.model.metadata.processes; + + +import java.io.Serializable; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput; +import org.eclipse.jetty.http.HttpStatus; + + +/******************************************************************************* + ** + *******************************************************************************/ +public interface ApiProcessOutputInterface +{ + + /******************************************************************************* + ** + *******************************************************************************/ + Serializable getOutputForProcess(RunProcessInput runProcessInput, RunProcessOutput runProcessOutput) throws QException; + + /******************************************************************************* + ** + *******************************************************************************/ + default HttpStatus.Code getSuccessStatusCode(RunProcessInput runProcessInput, RunProcessOutput runProcessOutput) + { + return (HttpStatus.Code.OK); + } + +} 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 new file mode 100644 index 00000000..9894f96a --- /dev/null +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessSummaryListOutput.java @@ -0,0 +1,138 @@ +package com.kingsrook.qqq.api.model.metadata.processes; + + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.logging.QLogger; +import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryFilterLink; +import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLine; +import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLineInterface; +import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryRecordLink; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import org.apache.commons.lang.NotImplementedException; +import org.eclipse.jetty.http.HttpStatus; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class ApiProcessSummaryListOutput implements ApiProcessOutputInterface +{ + private static final QLogger LOG = QLogger.getLogger(ApiProcessSummaryListOutput.class); + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public HttpStatus.Code getSuccessStatusCode(RunProcessInput runProcessInput, RunProcessOutput runProcessOutput) + { + List processSummaryLineInterfaces = (List) runProcessOutput.getValues().get("processResults"); + if(processSummaryLineInterfaces.isEmpty()) + { + ////////////////////////////////////////////////////////////////////////// + // if there are no summary lines, all we can return is 204 - no content // + ////////////////////////////////////////////////////////////////////////// + return (HttpStatus.Code.NO_CONTENT); + } + else + { + /////////////////////////////////////////////////////////////////////////////////// + // else if there are summary lines, we'll represent them as a 207 - multi-status // + /////////////////////////////////////////////////////////////////////////////////// + return (HttpStatus.Code.MULTI_STATUS); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public Serializable getOutputForProcess(RunProcessInput runProcessInput, RunProcessOutput runProcessOutput) throws QException + { + try + { + ArrayList apiOutput = new ArrayList<>(); + List processSummaryLineInterfaces = (List) runProcessOutput.getValues().get("processResults"); + for(ProcessSummaryLineInterface processSummaryLineInterface : processSummaryLineInterfaces) + { + if(processSummaryLineInterface instanceof ProcessSummaryLine processSummaryLine) + { + processSummaryLine.setCount(1); + processSummaryLine.prepareForFrontend(true); + + List primaryKeys = processSummaryLine.getPrimaryKeys(); + if(CollectionUtils.nullSafeHasContents(primaryKeys)) + { + for(Serializable primaryKey : primaryKeys) + { + HashMap map = toMap(processSummaryLine); + map.put("id", primaryKey); + apiOutput.add(map); + } + } + else + { + apiOutput.add(toMap(processSummaryLine)); + } + } + else if(processSummaryLineInterface instanceof ProcessSummaryRecordLink processSummaryRecordLink) + { + throw new NotImplementedException("ProcessSummaryRecordLink handling"); + } + else if(processSummaryLineInterface instanceof ProcessSummaryFilterLink processSummaryFilterLink) + { + throw new NotImplementedException("ProcessSummaryFilterLink handling"); + } + else + { + throw new NotImplementedException("Unknown ProcessSummaryLineInterface handling"); + } + } + + return (apiOutput); + } + catch(Exception e) + { + LOG.warn("Error getting api output for process", e); + throw (new QException("Error generating process output", e)); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @SuppressWarnings("checkstyle:indentation") + private static HashMap toMap(ProcessSummaryLine processSummaryLine) + { + HashMap map = new HashMap<>(); + HttpStatus.Code code = switch(processSummaryLine.getStatus()) + { + case OK, WARNING, INFO -> HttpStatus.Code.OK; + case ERROR -> HttpStatus.Code.INTERNAL_SERVER_ERROR; + }; + + String messagePrefix = switch(processSummaryLine.getStatus()) + { + case OK, INFO, ERROR -> ""; + case WARNING -> "Warning: "; + }; + + map.put("statusCode", code.getCode()); + map.put("statusText", code.getMessage()); + map.put("message", messagePrefix + processSummaryLine.getMessage()); + + return (map); + } + +} 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 new file mode 100644 index 00000000..dd1a1321 --- /dev/null +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessUtils.java @@ -0,0 +1,169 @@ +package com.kingsrook.qqq.api.model.metadata.processes; + + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import com.kingsrook.qqq.api.model.APIVersion; +import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaData; +import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaData; +import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaDataContainer; +import com.kingsrook.qqq.backend.core.context.QContext; +import com.kingsrook.qqq.backend.core.exceptions.QNotFoundException; +import com.kingsrook.qqq.backend.core.logging.LogPair; +import com.kingsrook.qqq.backend.core.logging.QLogger; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import com.kingsrook.qqq.backend.core.utils.Pair; +import com.kingsrook.qqq.backend.core.utils.StringUtils; +import org.apache.commons.lang.BooleanUtils; +import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class ApiProcessUtils +{ + private static final QLogger LOG = QLogger.getLogger(ApiProcessUtils.class); + + private static Map, Map> processApiNameMap = new HashMap<>(); + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static Pair getProcessMetaDataPair(ApiInstanceMetaData apiInstanceMetaData, String version, String processApiName) throws QNotFoundException + { + QProcessMetaData process = getProcessByApiName(apiInstanceMetaData.getName(), version, processApiName); + LogPair[] logPairs = new LogPair[] { logPair("apiName", apiInstanceMetaData.getName()), logPair("version", version), logPair("processApiName", processApiName) }; + + if(process == null) + { + LOG.info("404 because process is null (processApiName=" + processApiName + ")", logPairs); + throw (new QNotFoundException("Could not find a process named " + processApiName + " in this api.")); + } + + if(BooleanUtils.isTrue(process.getIsHidden())) + { + LOG.info("404 because process isHidden", logPairs); + throw (new QNotFoundException("Could not find a process named " + processApiName + " in this api.")); + } + + ApiProcessMetaDataContainer apiProcessMetaDataContainer = ApiProcessMetaDataContainer.of(process); + if(apiProcessMetaDataContainer == null) + { + LOG.info("404 because process apiProcessMetaDataContainer is null", logPairs); + throw (new QNotFoundException("Could not find a process named " + processApiName + " in this api.")); + } + + ApiProcessMetaData apiProcessMetaData = apiProcessMetaDataContainer.getApiProcessMetaData(apiInstanceMetaData.getName()); + if(apiProcessMetaData == null) + { + LOG.info("404 because process apiProcessMetaData is null", logPairs); + throw (new QNotFoundException("Could not find a process named " + processApiName + " in this api.")); + } + + if(BooleanUtils.isTrue(apiProcessMetaData.getIsExcluded())) + { + LOG.info("404 because process is excluded", logPairs); + throw (new QNotFoundException("Could not find a process named " + processApiName + " in this api.")); + } + + APIVersion requestApiVersion = new APIVersion(version); + List supportedVersions = apiInstanceMetaData.getSupportedVersions(); + if(CollectionUtils.nullSafeIsEmpty(supportedVersions) || !supportedVersions.contains(requestApiVersion)) + { + LOG.info("404 because requested version is not supported", logPairs); + throw (new QNotFoundException(version + " is not a supported version in this api.")); + } + + if(!apiProcessMetaData.getApiVersionRange().includes(requestApiVersion)) + { + LOG.info("404 because process version range does not include requested version", logPairs); + throw (new QNotFoundException(version + " is not a supported version for process " + processApiName + " in this api.")); + } + + return (Pair.of(apiProcessMetaData, process)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static QProcessMetaData getProcessByApiName(String apiName, String version, String processApiName) + { + ///////////////////////////////////////////////////////////////////////////////////////////// + // processApiNameMap is a map of (apiName,apiVersion) => Map. // + // that is to say, a 2-level map. The first level is keyed by (apiName,apiVersion) pairs. // + // the second level is keyed by processApiNames. // + ///////////////////////////////////////////////////////////////////////////////////////////// + Pair key = new Pair<>(apiName, version); + if(processApiNameMap.get(key) == null) + { + Map map = new HashMap<>(); + + for(QProcessMetaData process : QContext.getQInstance().getProcesses().values()) + { + ApiProcessMetaDataContainer apiProcessMetaDataContainer = ApiProcessMetaDataContainer.of(process); + if(apiProcessMetaDataContainer != null) + { + ApiProcessMetaData apiProcessMetaData = apiProcessMetaDataContainer.getApiProcessMetaData(apiName); + if(apiProcessMetaData != null) + { + String name = process.getName(); + if(StringUtils.hasContent(apiProcessMetaData.getApiProcessName())) + { + name = apiProcessMetaData.getApiProcessName(); + } + map.put(name, process); + } + } + } + + processApiNameMap.put(key, map); + } + + return (processApiNameMap.get(key).get(processApiName)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static String getProcessApiPath(QInstance qInstance, QProcessMetaData process, ApiProcessMetaData apiProcessMetaData, ApiInstanceMetaData apiInstanceMetaData) + { + if(StringUtils.hasContent(apiProcessMetaData.getPath())) + { + return apiProcessMetaData.getPath() + "/" + apiProcessMetaData.getApiProcessName(); + } + else if(StringUtils.hasContent(process.getTableName())) + { + QTableMetaData table = qInstance.getTable(process.getTableName()); + String tablePathPart = table.getName(); + ApiTableMetaDataContainer apiTableMetaDataContainer = ApiTableMetaDataContainer.of(table); + if(apiTableMetaDataContainer != null) + { + ApiTableMetaData apiTableMetaData = apiTableMetaDataContainer.getApis().get(apiInstanceMetaData.getName()); + if(apiTableMetaData != null) + { + if(StringUtils.hasContent(apiTableMetaData.getApiTableName())) + { + tablePathPart = apiTableMetaData.getApiTableName(); + } + } + } + return tablePathPart + "/" + apiProcessMetaData.getApiProcessName(); + } + else + { + return apiProcessMetaData.getApiProcessName(); + } + } + +} 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 fdef3cd5..20ea1005 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 @@ -29,8 +29,12 @@ import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaData; import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaDataContainer; import com.kingsrook.qqq.api.model.metadata.fields.ApiFieldMetaData; import com.kingsrook.qqq.api.model.metadata.fields.ApiFieldMetaDataContainer; +import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessInput; +import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessInputFieldsContainer; import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessMetaData; import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessMetaDataContainer; +import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessObjectOutput; +import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessSummaryListOutput; import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaData; import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaDataContainer; import com.kingsrook.qqq.api.model.openapi.HttpMethod; @@ -72,8 +76,10 @@ import com.kingsrook.qqq.backend.core.model.statusmessages.BadInputStatusMessage import com.kingsrook.qqq.backend.core.model.statusmessages.QWarningMessage; import com.kingsrook.qqq.backend.core.model.statusmessages.SystemErrorStatusMessage; import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryBackendModule; +import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.ExtractViaQueryStep; +import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.LoadViaUpdateStep; +import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.StreamedETLWithFrontendProcess; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; -import com.kingsrook.qqq.backend.core.utils.collections.ListBuilder; /******************************************************************************* @@ -89,7 +95,8 @@ public class TestUtils public static final String TABLE_NAME_LINE_ITEM_EXTRINSIC = "orderLineExtrinsic"; public static final String TABLE_NAME_ORDER_EXTRINSIC = "orderExtrinsic"; - public static final String PROCESS_NAME_GET_PERSON_INFO = "getPersonInfo"; + public static final String PROCESS_NAME_GET_PERSON_INFO = "getPersonInfo"; + public static final String PROCESS_NAME_TRANSFORM_PEOPLE = "transformPeople"; public static final String API_NAME = "test-api"; public static final String ALTERNATIVE_API_NAME = "person-api"; @@ -122,6 +129,7 @@ public class TestUtils qInstance.addPossibleValueSource(definePersonPossibleValueSource()); qInstance.addProcess(defineProcessGetPersonInfo()); + qInstance.addProcess(defineProcessTransformPeople()); qInstance.setAuthentication(new Auth0AuthenticationMetaData().withType(QAuthenticationType.FULLY_ANONYMOUS).withName("anonymous")); @@ -214,12 +222,12 @@ public class TestUtils .withApiProcessMetaData(API_NAME, new ApiProcessMetaData() .withInitialVersion(CURRENT_API_VERSION) .withMethod(HttpMethod.GET) - .withInferredInputFields(process) - .withOutputFields(ListBuilder.of( - new QFieldMetaData("density", QFieldType.DECIMAL), - new QFieldMetaData("daysOld", QFieldType.INTEGER), - new QFieldMetaData("nickname", QFieldType.STRING) - )) + .withInput(new ApiProcessInput() + .withQueryStringParams(new ApiProcessInputFieldsContainer().withInferredInputFields(process))) + .withOutput(new ApiProcessObjectOutput() + .withOutputField(new QFieldMetaData("density", QFieldType.DECIMAL)) + .withOutputField(new QFieldMetaData("daysOld", QFieldType.INTEGER)) + .withOutputField(new QFieldMetaData("nickname", QFieldType.STRING))) )); return (process); @@ -227,6 +235,35 @@ public class TestUtils + /******************************************************************************* + ** + *******************************************************************************/ + private static QProcessMetaData defineProcessTransformPeople() + { + QProcessMetaData process = StreamedETLWithFrontendProcess.processMetaDataBuilder() + .withName(PROCESS_NAME_TRANSFORM_PEOPLE) + .withTableName(TABLE_NAME_PERSON) + .withSourceTable(TABLE_NAME_PERSON) + .withDestinationTable(TABLE_NAME_PERSON) + .withMinInputRecords(1) + .withExtractStepClass(ExtractViaQueryStep.class) + .withTransformStepClass(TransformPersonStep.class) + .withLoadStepClass(LoadViaUpdateStep.class) + .getProcessMetaData(); + + process.withSupplementalMetaData(new ApiProcessMetaDataContainer() + .withApiProcessMetaData(API_NAME, new ApiProcessMetaData() + .withInitialVersion(CURRENT_API_VERSION) + .withMethod(HttpMethod.POST) + .withInput(new ApiProcessInput() + .withQueryStringParams(new ApiProcessInputFieldsContainer().withRecordIdsField(new QFieldMetaData("id", QFieldType.STRING)))) + .withOutput(new ApiProcessSummaryListOutput()))); + + return (process); + } + + + /******************************************************************************* ** Define the in-memory backend used in standard tests *******************************************************************************/ diff --git a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/TransformPersonStep.java b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/TransformPersonStep.java new file mode 100644 index 00000000..6abda809 --- /dev/null +++ b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/TransformPersonStep.java @@ -0,0 +1,59 @@ +package com.kingsrook.qqq.api; + + +import java.util.ArrayList; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLine; +import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLineInterface; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput; +import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.AbstractTransformStep; +import com.kingsrook.qqq.backend.core.processes.implementations.general.StandardProcessSummaryLineProducer; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class TransformPersonStep extends AbstractTransformStep +{ + private ProcessSummaryLine okLine = StandardProcessSummaryLineProducer.getOkToUpdateLine(); + private ProcessSummaryLine errorLine = StandardProcessSummaryLineProducer.getErrorLine(); + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public ArrayList getProcessSummary(RunBackendStepOutput runBackendStepOutput, boolean isForResultScreen) + { + ArrayList rs = new ArrayList<>(); + okLine.addSelfToListIfAnyCount(rs); + errorLine.addSelfToListIfAnyCount(rs); + return (rs); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException + { + for(QRecord record : runBackendStepInput.getRecords()) + { + Integer id = record.getValueInteger("id"); + if(id % 2 == 0) + { + okLine.incrementCountAndAddPrimaryKey(id); + } + else + { + errorLine.incrementCountAndAddPrimaryKey(id); + } + } + } + +} 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 b7889b1d..3db79362 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 @@ -1443,9 +1443,12 @@ class QJavalinApiHandlerTest extends BaseTest ** *******************************************************************************/ @Test - void testProcess() throws QException + void testGetProcessForObject() throws QException { - HttpResponse response = Unirest.get(BASE_URL + "/api/" + VERSION + "/person/getPersonInfo?age=43&partnerPersonId=1&heightInches=72&weightPounds=220&homeTown=Chester").asString(); + 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?age=43&partnerPersonId=1&heightInches=72&weightPounds=220&homeTown=Chester").asString(); assertEquals(HttpStatus.OK_200, response.getStatus()); JSONObject jsonObject = new JSONObject(response.getBody()); System.out.println(jsonObject.toString(3)); @@ -1453,6 +1456,33 @@ class QJavalinApiHandlerTest extends BaseTest + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testPostProcessForProcessSummaryList() throws QException + { + insertSimpsons(); + + HttpResponse response = Unirest.get(BASE_URL + "/api/" + VERSION + "/person/transformPeople").asString(); + assertErrorResponse(HttpStatus.METHOD_NOT_ALLOWED_405, "This path only supports method: POST", response); + + response = Unirest.post(BASE_URL + "/api/" + VERSION + "/person/transformPeople").asString(); + assertErrorResponse(HttpStatus.BAD_REQUEST_400, "Records to run through this process were not specified", response); + + response = Unirest.post(BASE_URL + "/api/" + VERSION + "/person/transformPeople?id=999").asString(); + assertEquals(HttpStatus.NO_CONTENT_204, response.getStatus()); + assertEquals("", response.getBody()); + + response = Unirest.post(BASE_URL + "/api/" + VERSION + "/person/transformPeople?id=1,2,3").asString(); + assertEquals(HttpStatus.MULTI_STATUS_207, response.getStatus()); + JSONArray jsonArray = new JSONArray(response.getBody()); + assertEquals(3, jsonArray.length()); + System.out.println(jsonArray.toString(3)); + } + + + /******************************************************************************* ** *******************************************************************************/