diff --git a/pom.xml b/pom.xml index 5347aa5a..20fde93f 100644 --- a/pom.xml +++ b/pom.xml @@ -44,7 +44,7 @@ - 0.15.0-SNAPSHOT + 0.16.0-SNAPSHOT UTF-8 UTF-8 diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/async/AsyncJobManager.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/async/AsyncJobManager.java index 2c16afd0..14b9fad6 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/async/AsyncJobManager.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/async/AsyncJobManager.java @@ -39,6 +39,7 @@ import com.kingsrook.qqq.backend.core.state.InMemoryStateProvider; import com.kingsrook.qqq.backend.core.state.StateProviderInterface; import com.kingsrook.qqq.backend.core.state.StateType; import com.kingsrook.qqq.backend.core.state.UUIDAndTypeStateKey; +import com.kingsrook.qqq.backend.core.utils.StringUtils; import org.apache.logging.log4j.Level; import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; @@ -50,6 +51,7 @@ public class AsyncJobManager { private static final QLogger LOG = QLogger.getLogger(AsyncJobManager.class); + private String forcedJobUUID = null; /******************************************************************************* @@ -69,7 +71,8 @@ public class AsyncJobManager *******************************************************************************/ public T startJob(String jobName, long timeout, TimeUnit timeUnit, AsyncJob asyncJob) throws JobGoingAsyncException, QException { - UUIDAndTypeStateKey uuidAndTypeStateKey = new UUIDAndTypeStateKey(UUID.randomUUID(), StateType.ASYNC_JOB_STATUS); + UUID jobUUID = StringUtils.hasContent(forcedJobUUID) ? UUID.fromString(forcedJobUUID) : UUID.randomUUID(); + UUIDAndTypeStateKey uuidAndTypeStateKey = new UUIDAndTypeStateKey(jobUUID, StateType.ASYNC_JOB_STATUS); AsyncJobStatus asyncJobStatus = new AsyncJobStatus(); asyncJobStatus.setState(AsyncJobState.RUNNING); getStateProvider().put(uuidAndTypeStateKey, asyncJobStatus); @@ -205,4 +208,35 @@ public class AsyncJobManager jobStatus.ifPresent(asyncJobStatus -> asyncJobStatus.setCancelRequested(true)); } + + + /******************************************************************************* + ** Getter for forcedJobUUID + *******************************************************************************/ + public String getForcedJobUUID() + { + return (this.forcedJobUUID); + } + + + + /******************************************************************************* + ** Setter for forcedJobUUID + *******************************************************************************/ + public void setForcedJobUUID(String forcedJobUUID) + { + this.forcedJobUUID = forcedJobUUID; + } + + + + /******************************************************************************* + ** Fluent setter for forcedJobUUID + *******************************************************************************/ + public AsyncJobManager withForcedJobUUID(String forcedJobUUID) + { + this.forcedJobUUID = forcedJobUUID; + return (this); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/audits/AuditAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/audits/AuditAction.java index 4bbd6881..41015e57 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/audits/AuditAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/audits/AuditAction.java @@ -76,6 +76,21 @@ public class AuditAction extends AbstractQActionFunction securityKeyValues, String message, List detailMessages) + { + List detailRecords = null; + if(CollectionUtils.nullSafeHasContents(detailMessages)) + { + detailRecords = detailMessages.stream().map(m -> new QRecord().withValue("message", m)).toList(); + } + execute(tableName, recordId, securityKeyValues, message, detailRecords); + } + + + /******************************************************************************* ** Execute to insert 1 audit, with a list of detail child records *******************************************************************************/ diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/polling/PollingAutomationPerTableRunner.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/polling/PollingAutomationPerTableRunner.java index 2f0b3106..52cc7646 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/polling/PollingAutomationPerTableRunner.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/polling/PollingAutomationPerTableRunner.java @@ -39,12 +39,15 @@ import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader; 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.reporting.RecordPipe; +import com.kingsrook.qqq.backend.core.actions.tables.GetAction; import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput; import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput; +import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy; @@ -61,11 +64,14 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.Automatio import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.QTableAutomationDetails; import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.TableAutomationAction; import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.TriggerEvent; +import com.kingsrook.qqq.backend.core.model.savedfilters.SavedFilter; import com.kingsrook.qqq.backend.core.model.session.QSession; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import com.kingsrook.qqq.backend.core.utils.JsonUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder; import org.apache.commons.lang.NotImplementedException; +import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; /******************************************************************************* @@ -270,16 +276,39 @@ public class PollingAutomationPerTableRunner implements Runnable QueryOutput queryOutput = new QueryAction().execute(queryInput); for(QRecord record : queryOutput.getRecords()) { - // todo - get filter if there is/was one - rs.add(new TableAutomationAction() - .withName("Script:" + record.getValue("scriptId")) - .withFilter(null) - .withTriggerEvent(triggerEvent) - .withPriority(record.getValueInteger("priority")) - .withCodeReference(new QCodeReference(RunRecordScriptAutomationHandler.class)) - .withValues(MapBuilder.of("scriptId", record.getValue("scriptId"))) - .withIncludeRecordAssociations(true) - ); + TableTrigger tableTrigger = new TableTrigger(record); + + try + { + QQueryFilter filter = null; + Integer filterId = tableTrigger.getFilterId(); + if(filterId != null) + { + GetInput getInput = new GetInput(); + getInput.setTableName(SavedFilter.TABLE_NAME); + getInput.setPrimaryKey(filterId); + GetOutput getOutput = new GetAction().execute(getInput); + if(getOutput.getRecord() != null) + { + SavedFilter savedFilter = new SavedFilter(getOutput.getRecord()); + filter = JsonUtils.toObject(savedFilter.getFilterJson(), QQueryFilter.class); + } + } + + rs.add(new TableAutomationAction() + .withName("Script:" + tableTrigger.getScriptId()) + .withFilter(filter) + .withTriggerEvent(triggerEvent) + .withPriority(tableTrigger.getPriority()) + .withCodeReference(new QCodeReference(RunRecordScriptAutomationHandler.class)) + .withValues(MapBuilder.of("scriptId", tableTrigger.getScriptId())) + .withIncludeRecordAssociations(true) + ); + } + catch(Exception e) + { + LOG.error("Error setting up table trigger", e, logPair("tableTriggerId", tableTrigger.getId())); + } } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/ExecuteCodeAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/ExecuteCodeAction.java index c142fc1f..875cf2f5 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/ExecuteCodeAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/ExecuteCodeAction.java @@ -101,6 +101,17 @@ public class ExecuteCodeAction context.putAll(input.getInput()); } + ///////////////////////////////////////////////////////////////////////////////// + // set the qCodeExecutor into any context objects which are QCodeExecutorAware // + ///////////////////////////////////////////////////////////////////////////////// + for(Serializable value : context.values()) + { + if(value instanceof QCodeExecutorAware qCodeExecutorAware) + { + qCodeExecutorAware.setQCodeExecutor(qCodeExecutor); + } + } + Serializable codeOutput = qCodeExecutor.execute(codeReference, context, executionLogger); output.setOutput(codeOutput); executionLogger.acceptExecutionEnd(codeOutput); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/QCodeExecutor.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/QCodeExecutor.java index e6cd808a..a0e06f81 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/QCodeExecutor.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/QCodeExecutor.java @@ -41,4 +41,14 @@ public interface QCodeExecutor *******************************************************************************/ Serializable execute(QCodeReference codeReference, Map inputContext, QCodeExecutionLoggerInterface executionLogger) throws QCodeException; + /******************************************************************************* + ** Process an object from the script's language/runtime into a (more) native java object. + ** e.g., a Nashorn ScriptObjectMirror will end up as a "primitive", or a List or Map of such + ** + *******************************************************************************/ + default Object convertObjectToJava(Object object) + { + return (object); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/QCodeExecutorAware.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/QCodeExecutorAware.java new file mode 100644 index 00000000..e15dd2a6 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/QCodeExecutorAware.java @@ -0,0 +1,36 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.actions.scripts; + + +/******************************************************************************* + ** Interface for classes that can accept a QCodeExecutor object via a setter. + *******************************************************************************/ +public interface QCodeExecutorAware +{ + + /******************************************************************************* + ** + *******************************************************************************/ + void setQCodeExecutor(QCodeExecutor qCodeExecutor); + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QValueFormatter.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QValueFormatter.java index c58d78cc..85691e74 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QValueFormatter.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QValueFormatter.java @@ -136,7 +136,7 @@ public class QValueFormatter { return formatValue(displayFormat, ValueUtils.getValueAsBigDecimal(value)); } - else if(e.getMessage().equals("d != java.math.BigDecimal")) + else if(e.getMessage().equals("d != java.math.BigDecimal") || e.getMessage().equals("d != java.lang.String")) { return formatValue(displayFormat, ValueUtils.getValueAsInteger(value)); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/exceptions/QException.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/exceptions/QException.java index 651919fa..67b4aba6 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/exceptions/QException.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/exceptions/QException.java @@ -22,12 +22,19 @@ package com.kingsrook.qqq.backend.core.exceptions; +import org.apache.logging.log4j.Level; + + /******************************************************************************* * Base class for checked exceptions thrown in qqq. * *******************************************************************************/ public class QException extends Exception { + private boolean hasLoggedWarning; + private boolean hasLoggedError; + + /******************************************************************************* ** Constructor of message @@ -59,4 +66,102 @@ public class QException extends Exception { super(message, cause); } + + + + /******************************************************************************* + ** Getter for hasLoggedWarning + *******************************************************************************/ + public boolean getHasLoggedWarning() + { + return (this.hasLoggedWarning); + } + + + + /******************************************************************************* + ** Setter for hasLoggedWarning + *******************************************************************************/ + public void setHasLoggedWarning(boolean hasLoggedWarning) + { + this.hasLoggedWarning = hasLoggedWarning; + } + + + + /******************************************************************************* + ** Fluent setter for hasLoggedWarning + *******************************************************************************/ + public QException withHasLoggedWarning(boolean hasLoggedWarning) + { + this.hasLoggedWarning = hasLoggedWarning; + return (this); + } + + + + /******************************************************************************* + ** Getter for hasLoggedError + *******************************************************************************/ + public boolean getHasLoggedError() + { + return (this.hasLoggedError); + } + + + + /******************************************************************************* + ** Setter for hasLoggedError + *******************************************************************************/ + public void setHasLoggedError(boolean hasLoggedError) + { + this.hasLoggedError = hasLoggedError; + } + + + + /******************************************************************************* + ** Fluent setter for hasLoggedError + *******************************************************************************/ + public QException withHasLoggedError(boolean hasLoggedError) + { + this.hasLoggedError = hasLoggedError; + return (this); + } + + + + /******************************************************************************* + ** helper function for getting if level logged + *******************************************************************************/ + public boolean hasLoggedLevel(Level level) + { + if(Level.WARN.equals(level)) + { + return (hasLoggedWarning); + } + if(Level.ERROR.equals(level)) + { + return (hasLoggedError); + } + return (false); + } + + + + /******************************************************************************* + ** helper function for setting if level logged + *******************************************************************************/ + public void setHasLoggedLevel(Level level) + { + if(Level.WARN.equals(level)) + { + setHasLoggedWarning(true); + } + if(Level.ERROR.equals(level)) + { + setHasLoggedError(true); + } + } + } 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 587addbb..81a64968 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 @@ -27,6 +27,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; @@ -57,12 +58,13 @@ import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendComponen 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.model.metadata.processes.QSupplementalProcessMetaData; import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportDataSource; import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData; import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportView; import com.kingsrook.qqq.backend.core.model.metadata.tables.ExposedJoin; import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection; -import com.kingsrook.qqq.backend.core.model.metadata.tables.QMiddlewareTableMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QSupplementalTableMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier; import com.kingsrook.qqq.backend.core.processes.implementations.bulk.delete.BulkDeleteLoadStep; @@ -96,6 +98,13 @@ public class QInstanceEnricher ////////////////////////////////////////////////////////// private boolean configRemoveIdFromNameWhenCreatingPossibleValueFieldLabels = true; + ////////////////////////////////////////////////////////////////////////////////////////////////// + // let an instance define mappings to be applied during name-to-label enrichments, // + // e.g., to avoid ever incorrectly camel-casing an acronym (e.g., "Tla" shoudl always be "TLA") // + // or to expand abbreviations in code (e.g., "Addr" should always be "Address" // + ////////////////////////////////////////////////////////////////////////////////////////////////// + private static final Map labelMappings = new LinkedHashMap<>(); + /******************************************************************************* @@ -261,9 +270,9 @@ public class QInstanceEnricher { table.getFields().values().forEach(this::enrichField); - for(QMiddlewareTableMetaData middlewareTableMetaData : CollectionUtils.nonNullMap(table.getMiddlewareMetaData()).values()) + for(QSupplementalTableMetaData supplementalTableMetaData : CollectionUtils.nonNullMap(table.getSupplementalMetaData()).values()) { - middlewareTableMetaData.enrich(table); + supplementalTableMetaData.enrich(table); } } @@ -367,6 +376,11 @@ public class QInstanceEnricher process.getStepList().forEach(this::enrichStep); } + for(QSupplementalProcessMetaData supplementalProcessMetaData : CollectionUtils.nonNullMap(process.getSupplementalMetaData()).values()) + { + supplementalProcessMetaData.enrich(this, process); + } + enrichPermissionRules(process); } @@ -641,7 +655,17 @@ public class QInstanceEnricher //////////////////////////////////////////////////////////////// .replaceAll("([0-9])([A-Za-z])", "$1 $2"); - return (name.substring(0, 1).toUpperCase(Locale.ROOT) + suffix); + String label = name.substring(0, 1).toUpperCase(Locale.ROOT) + suffix; + + ///////////////////////////////////////////////////////////////////////////////////////////// + // apply any label mappings - e.g., to force app-specific acronyms/initialisms to all-caps // + ///////////////////////////////////////////////////////////////////////////////////////////// + for(Map.Entry entry : labelMappings.entrySet()) + { + label = label.replaceAll(entry.getKey(), entry.getValue()); + } + + return (label); } @@ -1105,4 +1129,35 @@ public class QInstanceEnricher { return (this.joinGraph); } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static void addLabelMapping(String from, String to) + { + labelMappings.put(from, to); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static void removeLabelMapping(String from) + { + labelMappings.remove(from); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static void clearLabelMappings() + { + labelMappings.clear(); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java index 3bc59276..5b65f4c1 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java @@ -48,7 +48,7 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin; import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; -import com.kingsrook.qqq.backend.core.model.metadata.QMiddlewareInstanceMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.QSupplementalInstanceMetaData; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType; import com.kingsrook.qqq.backend.core.model.metadata.fields.AdornmentType; @@ -63,6 +63,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppSection; import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QSupplementalProcessMetaData; import com.kingsrook.qqq.backend.core.model.metadata.queues.SQSQueueProviderMetaData; import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportDataSource; import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportField; @@ -158,7 +159,7 @@ public class QInstanceValidator validateQueuesAndProviders(qInstance); validateJoins(qInstance); validateSecurityKeyTypes(qInstance); - validateMiddlewareMetaData(qInstance); + validateSupplementalMetaData(qInstance); validateUniqueTopLevelNames(qInstance); } @@ -182,11 +183,11 @@ public class QInstanceValidator /******************************************************************************* ** *******************************************************************************/ - private void validateMiddlewareMetaData(QInstance qInstance) + private void validateSupplementalMetaData(QInstance qInstance) { - for(QMiddlewareInstanceMetaData middlewareInstanceMetaData : CollectionUtils.nonNullMap(qInstance.getMiddlewareMetaData()).values()) + for(QSupplementalInstanceMetaData supplementalInstanceMetaData : CollectionUtils.nonNullMap(qInstance.getSupplementalMetaData()).values()) { - middlewareInstanceMetaData.validate(qInstance, this); + supplementalInstanceMetaData.validate(qInstance, this); } } @@ -572,6 +573,11 @@ public class QInstanceValidator RECORD_SECURITY_LOCKS_LOOP: for(RecordSecurityLock recordSecurityLock : CollectionUtils.nonNullList(table.getRecordSecurityLocks())) { + if(!assertCondition(recordSecurityLock != null, prefix + "has a null recordSecurityLock (did you mean to give it a null list of locks?)")) + { + continue; + } + String securityKeyTypeName = recordSecurityLock.getSecurityKeyType(); if(assertCondition(StringUtils.hasContent(securityKeyTypeName), prefix + "has a recordSecurityLock that is missing a securityKeyType")) { @@ -1226,6 +1232,11 @@ public class QInstanceValidator } } + for(QSupplementalProcessMetaData supplementalProcessMetaData : CollectionUtils.nonNullMap(process.getSupplementalMetaData()).values()) + { + supplementalProcessMetaData.validate(qInstance, process, this); + } + }); } } @@ -1703,7 +1714,7 @@ public class QInstanceValidator ** But if it throws, add the provided message to the list of errors (and return false, ** e.g., in case you need to stop evaluating rules to avoid exceptions). *******************************************************************************/ - private boolean assertNoException(UnsafeLambda unsafeLambda, String message) + public boolean assertNoException(UnsafeLambda unsafeLambda, String message) { try { @@ -1736,7 +1747,7 @@ public class QInstanceValidator /******************************************************************************* ** *******************************************************************************/ - private void warn(String message) + public void warn(String message) { if(printWarnings) { diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/logging/QLogger.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/logging/QLogger.java index d057b8fb..55d58086 100755 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/logging/QLogger.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/logging/QLogger.java @@ -29,10 +29,12 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import com.kingsrook.qqq.backend.core.context.QContext; +import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter; import com.kingsrook.qqq.backend.core.model.session.QSession; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import com.kingsrook.qqq.backend.core.utils.ExceptionUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils; -import org.apache.commons.lang3.exception.ExceptionUtils; import org.apache.logging.log4j.Level; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -392,7 +394,7 @@ public class QLogger *******************************************************************************/ public void warn(String message, Throwable t) { - logger.warn(makeJsonString(message, t)); + logger.log(determineIfShouldDowngrade(t, Level.WARN), makeJsonString(message, t)); } @@ -402,7 +404,7 @@ public class QLogger *******************************************************************************/ public void warn(String message, Throwable t, LogPair... logPairs) { - logger.warn(makeJsonString(message, t, logPairs)); + logger.log(determineIfShouldDowngrade(t, Level.WARN), makeJsonString(message, t, logPairs)); } @@ -412,7 +414,7 @@ public class QLogger *******************************************************************************/ public void warn(Throwable t) { - logger.warn(makeJsonString(null, t)); + logger.log(determineIfShouldDowngrade(t, Level.WARN), makeJsonString(null, t)); } @@ -452,7 +454,7 @@ public class QLogger *******************************************************************************/ public void error(String message, Throwable t) { - logger.error(makeJsonString(message, t)); + logger.log(determineIfShouldDowngrade(t, Level.ERROR), makeJsonString(message, t)); } @@ -462,7 +464,7 @@ public class QLogger *******************************************************************************/ public void error(String message, Throwable t, LogPair... logPairs) { - logger.error(makeJsonString(message, t, logPairs)); + logger.log(determineIfShouldDowngrade(t, Level.ERROR), makeJsonString(message, t, logPairs)); } @@ -472,7 +474,7 @@ public class QLogger *******************************************************************************/ public void error(Throwable t) { - logger.error(makeJsonString(null, t)); + logger.log(determineIfShouldDowngrade(t, Level.ERROR), makeJsonString(null, t)); } @@ -532,7 +534,7 @@ public class QLogger if(t != null) { - logPairList.add(logPair("stackTrace", LogUtils.filterStackTrace(ExceptionUtils.getStackTrace(t)))); + logPairList.add(logPair("stackTrace", LogUtils.filterStackTrace(org.apache.commons.lang3.exception.ExceptionUtils.getStackTrace(t)))); } return (LogUtils.jsonLog(logPairList)); @@ -582,4 +584,40 @@ public class QLogger } } } + + + + /******************************************************************************* + ** + *******************************************************************************/ + protected Level determineIfShouldDowngrade(Throwable t, Level level) + { + ////////////////////////////////////////////////////////////////////////////////////// + // look for QExceptions in the chain, if none found, return the log level passed in // + ////////////////////////////////////////////////////////////////////////////////////// + List exceptionList = ExceptionUtils.getClassListFromRootChain(t, QException.class); + if(CollectionUtils.nullSafeIsEmpty(exceptionList)) + { + return (level); + } + + //////////////////////////////////////////////////////////////////// + // check if any QException in this chain to see if it has already // + // logged this level, if so, downgrade to INFO // + //////////////////////////////////////////////////////////////////// + for(QException qException : exceptionList) + { + if(qException.hasLoggedLevel(level)) + { + log(Level.DEBUG, "Downgrading log message from " + level.toString() + " to " + Level.INFO, t); + return (Level.INFO); + } + } + + /////////////////////////////////////////////////////////////////////////////////////////////////// + // if it has not logged at this level, set that it has in QException, and return passed in level // + /////////////////////////////////////////////////////////////////////////////////////////////////// + exceptionList.get(0).setHasLoggedLevel(level); + return (level); + } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/ProcessSummaryFilterLink.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/ProcessSummaryFilterLink.java index b13e69e3..eee92ff2 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/ProcessSummaryFilterLink.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/ProcessSummaryFilterLink.java @@ -22,8 +22,10 @@ package com.kingsrook.qqq.backend.core.model.actions.processes; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.kingsrook.qqq.backend.core.logging.LogPair; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; +import com.kingsrook.qqq.backend.core.utils.StringUtils; import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; @@ -76,6 +78,35 @@ public class ProcessSummaryFilterLink implements ProcessSummaryLineInterface + /******************************************************************************* + ** + *******************************************************************************/ + @JsonIgnore + public String getFullText() + { + StringBuilder rs = new StringBuilder(); + + if(StringUtils.hasContent(linkPreText)) + { + rs.append(linkPreText).append(" "); + } + + if(StringUtils.hasContent(linkText)) + { + rs.append(linkText).append(" "); + } + + if(StringUtils.hasContent(linkPostText)) + { + rs.append(linkPostText).append(" "); + } + + rs.deleteCharAt(rs.length() - 1); + return (rs.toString()); + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/ProcessSummaryLine.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/ProcessSummaryLine.java index 39ead861..786318e0 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/ProcessSummaryLine.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/ProcessSummaryLine.java @@ -26,6 +26,7 @@ import java.io.Serializable; import java.util.ArrayList; import java.util.List; import com.kingsrook.qqq.backend.core.logging.LogPair; +import com.kingsrook.qqq.backend.core.utils.ObjectUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils; import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; @@ -395,15 +396,19 @@ public class ProcessSummaryLine implements ProcessSummaryLineInterface { if(count != null) { + String baseMessage; if(count.equals(1)) { - setMessage((isPast ? getSingularPastMessage() : getSingularFutureMessage()) - + (messageSuffix == null ? "" : messageSuffix)); + baseMessage = isPast ? getSingularPastMessage() : getSingularFutureMessage(); } else { - setMessage((isPast ? getPluralPastMessage() : getPluralFutureMessage()) - + (messageSuffix == null ? "" : messageSuffix)); + baseMessage = isPast ? getPluralPastMessage() : getPluralFutureMessage(); + } + + if(StringUtils.hasContent(baseMessage)) + { + setMessage(baseMessage + ObjectUtils.requireConditionElse(messageSuffix, StringUtils::hasContent, "")); } } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/ProcessSummaryRecordLink.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/ProcessSummaryRecordLink.java index fa8ae313..50e2c378 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/ProcessSummaryRecordLink.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/ProcessSummaryRecordLink.java @@ -23,7 +23,9 @@ package com.kingsrook.qqq.backend.core.model.actions.processes; import java.io.Serializable; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.kingsrook.qqq.backend.core.logging.LogPair; +import com.kingsrook.qqq.backend.core.utils.StringUtils; import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; @@ -64,6 +66,35 @@ public class ProcessSummaryRecordLink implements ProcessSummaryLineInterface + /******************************************************************************* + ** + *******************************************************************************/ + @JsonIgnore + public String getFullText() + { + StringBuilder rs = new StringBuilder(); + + if(StringUtils.hasContent(linkPreText)) + { + rs.append(linkPreText).append(" "); + } + + if(StringUtils.hasContent(linkText)) + { + rs.append(linkText).append(" "); + } + + if(StringUtils.hasContent(linkPostText)) + { + rs.append(linkPostText).append(" "); + } + + rs.deleteCharAt(rs.length() - 1); + return (rs.toString()); + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunProcessInput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunProcessInput.java index ba9ac596..a099caaf 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunProcessInput.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunProcessInput.java @@ -23,6 +23,10 @@ package com.kingsrook.qqq.backend.core.model.actions.processes; import java.io.Serializable; +import java.math.BigDecimal; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalTime; import java.util.List; import java.util.Map; import com.kingsrook.qqq.backend.core.actions.async.AsyncJobCallback; @@ -31,6 +35,7 @@ import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput; import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; +import com.kingsrook.qqq.backend.core.utils.ValueUtils; /******************************************************************************* @@ -194,6 +199,17 @@ public class RunProcessInput extends AbstractActionInput + /******************************************************************************* + ** + *******************************************************************************/ + public RunProcessInput withValue(String fieldName, Serializable value) + { + this.processState.getValues().put(fieldName, value); + return (this); + } + + + /******************************************************************************* ** Setter for values ** @@ -269,7 +285,7 @@ public class RunProcessInput extends AbstractActionInput *******************************************************************************/ public String getValueString(String fieldName) { - return ((String) getValue(fieldName)); + return (ValueUtils.getValueAsString(getValue(fieldName))); } @@ -280,7 +296,67 @@ public class RunProcessInput extends AbstractActionInput *******************************************************************************/ public Integer getValueInteger(String fieldName) { - return ((Integer) getValue(fieldName)); + return (ValueUtils.getValueAsInteger(getValue(fieldName))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public BigDecimal getValueBigDecimal(String fieldName) + { + return (ValueUtils.getValueAsBigDecimal(getValue(fieldName))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public Boolean getValueBoolean(String fieldName) + { + return (ValueUtils.getValueAsBoolean(getValue(fieldName))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public LocalTime getValueLocalTime(String fieldName) + { + return (ValueUtils.getValueAsLocalTime(getValue(fieldName))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public LocalDate getValueLocalDate(String fieldName) + { + return (ValueUtils.getValueAsLocalDate(getValue(fieldName))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public byte[] getValueByteArray(String fieldName) + { + return (ValueUtils.getValueAsByteArray(getValue(fieldName))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public Instant getValueInstant(String fieldName) + { + return (ValueUtils.getValueAsInstant(getValue(fieldName))); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunProcessOutput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunProcessOutput.java index c088f48b..466e02c4 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunProcessOutput.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunProcessOutput.java @@ -23,11 +23,16 @@ package com.kingsrook.qqq.backend.core.model.actions.processes; import java.io.Serializable; +import java.math.BigDecimal; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalTime; import java.util.List; import java.util.Map; import java.util.Optional; import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput; import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.utils.ValueUtils; /******************************************************************************* @@ -122,6 +127,99 @@ public class RunProcessOutput extends AbstractActionOutput implements Serializab + /******************************************************************************* + ** Getter for a single field's value + ** + *******************************************************************************/ + public Serializable getValue(String fieldName) + { + return (this.processState.getValues().get(fieldName)); + } + + + + /******************************************************************************* + ** Getter for a single field's value + ** + *******************************************************************************/ + public String getValueString(String fieldName) + { + return (ValueUtils.getValueAsString(getValue(fieldName))); + } + + + + /******************************************************************************* + ** Getter for a single field's value + ** + *******************************************************************************/ + public Integer getValueInteger(String fieldName) + { + return (ValueUtils.getValueAsInteger(getValue(fieldName))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public BigDecimal getValueBigDecimal(String fieldName) + { + return (ValueUtils.getValueAsBigDecimal(getValue(fieldName))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public Boolean getValueBoolean(String fieldName) + { + return (ValueUtils.getValueAsBoolean(getValue(fieldName))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public LocalTime getValueLocalTime(String fieldName) + { + return (ValueUtils.getValueAsLocalTime(getValue(fieldName))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public LocalDate getValueLocalDate(String fieldName) + { + return (ValueUtils.getValueAsLocalDate(getValue(fieldName))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public byte[] getValueByteArray(String fieldName) + { + return (ValueUtils.getValueAsByteArray(getValue(fieldName))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public Instant getValueInstant(String fieldName) + { + return (ValueUtils.getValueAsInstant(getValue(fieldName))); + } + + + /******************************************************************************* ** Setter for values ** @@ -133,6 +231,17 @@ public class RunProcessOutput extends AbstractActionOutput implements Serializab + /******************************************************************************* + ** + *******************************************************************************/ + public RunProcessOutput withValue(String fieldName, Serializable value) + { + this.processState.getValues().put(fieldName, value); + return (this); + } + + + /******************************************************************************* ** Setter for values ** diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/aggregate/AggregateOperator.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/aggregate/AggregateOperator.java index 513e95c7..b530fe23 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/aggregate/AggregateOperator.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/aggregate/AggregateOperator.java @@ -56,4 +56,14 @@ public enum AggregateOperator { return sqlPrefix; } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public Aggregate of(String fieldName) + { + return (new Aggregate(fieldName, this)); + } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/automation/TableTrigger.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/automation/TableTrigger.java index a21aedf0..a3c82b4f 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/automation/TableTrigger.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/automation/TableTrigger.java @@ -28,6 +28,7 @@ import com.kingsrook.qqq.backend.core.model.data.QField; import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.data.QRecordEntity; import com.kingsrook.qqq.backend.core.model.metadata.tables.TablesPossibleValueSourceMetaDataProvider; +import com.kingsrook.qqq.backend.core.model.savedfilters.SavedFilter; import com.kingsrook.qqq.backend.core.model.scripts.Script; @@ -50,7 +51,7 @@ public class TableTrigger extends QRecordEntity @QField(possibleValueSourceName = TablesPossibleValueSourceMetaDataProvider.NAME) private String tableName; - @QField(/* todo possibleValueSourceName = */) + @QField(possibleValueSourceName = SavedFilter.TABLE_NAME) private Integer filterId; @QField(possibleValueSourceName = Script.TABLE_NAME) diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QInstance.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QInstance.java index 0b9f45ee..4c05c923 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QInstance.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QInstance.java @@ -91,7 +91,7 @@ public class QInstance private Map queueProviders = new LinkedHashMap<>(); private Map queues = new LinkedHashMap<>(); - private Map middlewareMetaData = new LinkedHashMap<>(); + private Map supplementalMetaData = new LinkedHashMap<>(); private Map environmentValues = new LinkedHashMap<>(); private String defaultTimeZoneId = "UTC"; @@ -1083,60 +1083,60 @@ public class QInstance /******************************************************************************* - ** Getter for middlewareMetaData + ** Getter for supplementalMetaData *******************************************************************************/ - public Map getMiddlewareMetaData() + public Map getSupplementalMetaData() { - return (this.middlewareMetaData); + return (this.supplementalMetaData); } /******************************************************************************* - ** Getter for middlewareMetaData + ** Getter for supplementalMetaData *******************************************************************************/ - public QMiddlewareInstanceMetaData getMiddlewareMetaData(String type) + public QSupplementalInstanceMetaData getSupplementalMetaData(String type) { - if(this.middlewareMetaData == null) + if(this.supplementalMetaData == null) { return (null); } - return this.middlewareMetaData.get(type); + return this.supplementalMetaData.get(type); } /******************************************************************************* - ** Setter for middlewareMetaData + ** Setter for supplementalMetaData *******************************************************************************/ - public void setMiddlewareMetaData(Map middlewareMetaData) + public void setSupplementalMetaData(Map supplementalMetaData) { - this.middlewareMetaData = middlewareMetaData; + this.supplementalMetaData = supplementalMetaData; } /******************************************************************************* - ** Fluent setter for middlewareMetaData + ** Fluent setter for supplementalMetaData *******************************************************************************/ - public QInstance withMiddlewareMetaData(Map middlewareMetaData) + public QInstance withSupplementalMetaData(Map supplementalMetaData) { - this.middlewareMetaData = middlewareMetaData; + this.supplementalMetaData = supplementalMetaData; return (this); } /******************************************************************************* - ** Fluent setter for middlewareMetaData + ** Fluent setter for supplementalMetaData *******************************************************************************/ - public QInstance withMiddlewareMetaData(QMiddlewareInstanceMetaData middlewareMetaData) + public QInstance withSupplementalMetaData(QSupplementalInstanceMetaData supplementalMetaData) { - if(this.middlewareMetaData == null) + if(this.supplementalMetaData == null) { - this.middlewareMetaData = new HashMap<>(); + this.supplementalMetaData = new HashMap<>(); } - this.middlewareMetaData.put(middlewareMetaData.getType(), middlewareMetaData); + this.supplementalMetaData.put(supplementalMetaData.getType(), supplementalMetaData); return (this); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QMiddlewareInstanceMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QSupplementalInstanceMetaData.java similarity index 92% rename from qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QMiddlewareInstanceMetaData.java rename to qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QSupplementalInstanceMetaData.java index 5c20f8bf..709417e2 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QMiddlewareInstanceMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QSupplementalInstanceMetaData.java @@ -27,9 +27,10 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; /******************************************************************************* - ** Base-class for instance-level meta-data defined for a specific middleware. + ** Base-class for instance-level meta-data defined by some supplemental module, etc, + ** outside of qqq core *******************************************************************************/ -public abstract class QMiddlewareInstanceMetaData +public abstract class QSupplementalInstanceMetaData { protected String type; @@ -58,7 +59,7 @@ public abstract class QMiddlewareInstanceMetaData /******************************************************************************* ** Fluent setter for type *******************************************************************************/ - public QMiddlewareInstanceMetaData withType(String type) + public QSupplementalInstanceMetaData withType(String type) { this.type = type; return (this); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldMetaData.java index e99e5db0..44d023d5 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldMetaData.java @@ -85,7 +85,7 @@ public class QFieldMetaData implements Cloneable private List adornments; - private Map middlewareMetaData; + private Map supplementalMetaData; @@ -840,60 +840,60 @@ public class QFieldMetaData implements Cloneable /******************************************************************************* - ** Getter for middlewareMetaData + ** Getter for supplementalMetaData *******************************************************************************/ - public Map getMiddlewareMetaData() + public Map getSupplementalMetaData() { - return (this.middlewareMetaData); + return (this.supplementalMetaData); } /******************************************************************************* - ** Getter for middlewareMetaData + ** Getter for supplementalMetaData *******************************************************************************/ - public QMiddlewareFieldMetaData getMiddlewareMetaData(String type) + public QSupplementalFieldMetaData getSupplementalMetaData(String type) { - if(this.middlewareMetaData == null) + if(this.supplementalMetaData == null) { return (null); } - return this.middlewareMetaData.get(type); + return this.supplementalMetaData.get(type); } /******************************************************************************* - ** Setter for middlewareMetaData + ** Setter for supplementalMetaData *******************************************************************************/ - public void setMiddlewareMetaData(Map middlewareMetaData) + public void setSupplementalMetaData(Map supplementalMetaData) { - this.middlewareMetaData = middlewareMetaData; + this.supplementalMetaData = supplementalMetaData; } /******************************************************************************* - ** Fluent setter for middlewareMetaData + ** Fluent setter for supplementalMetaData *******************************************************************************/ - public QFieldMetaData withMiddlewareMetaData(Map middlewareMetaData) + public QFieldMetaData withSupplementalMetaData(Map supplementalMetaData) { - this.middlewareMetaData = middlewareMetaData; + this.supplementalMetaData = supplementalMetaData; return (this); } /******************************************************************************* - ** Fluent setter for middlewareMetaData + ** Fluent setter for supplementalMetaData *******************************************************************************/ - public QFieldMetaData withMiddlewareMetaData(QMiddlewareFieldMetaData middlewareMetaData) + public QFieldMetaData withSupplementalMetaData(QSupplementalFieldMetaData supplementalMetaData) { - if(this.middlewareMetaData == null) + if(this.supplementalMetaData == null) { - this.middlewareMetaData = new HashMap<>(); + this.supplementalMetaData = new HashMap<>(); } - this.middlewareMetaData.put(middlewareMetaData.getType(), middlewareMetaData); + this.supplementalMetaData.put(supplementalMetaData.getType(), supplementalMetaData); return (this); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QMiddlewareFieldMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QSupplementalFieldMetaData.java similarity index 90% rename from qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QMiddlewareFieldMetaData.java rename to qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QSupplementalFieldMetaData.java index 454187d4..16200ba2 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QMiddlewareFieldMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QSupplementalFieldMetaData.java @@ -23,9 +23,10 @@ package com.kingsrook.qqq.backend.core.model.metadata.fields; /******************************************************************************* - ** Base-class for field-level meta-data defined for a specific middleware. + ** Base-class for field-level meta-data defined by some supplemental module, etc, + ** outside of qqq core *******************************************************************************/ -public abstract class QMiddlewareFieldMetaData +public abstract class QSupplementalFieldMetaData { protected String type; @@ -54,7 +55,7 @@ public abstract class QMiddlewareFieldMetaData /******************************************************************************* ** Fluent setter for type *******************************************************************************/ - public QMiddlewareFieldMetaData withType(String type) + public QSupplementalFieldMetaData withType(String type) { this.type = type; return (this); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/possiblevalues/QPossibleValueSource.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/possiblevalues/QPossibleValueSource.java index 76a03b90..c0d0076b 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/possiblevalues/QPossibleValueSource.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/possiblevalues/QPossibleValueSource.java @@ -77,6 +77,21 @@ public class QPossibleValueSource implements TopLevelMetaDataInterface + /******************************************************************************* + ** Create a new possible value source, for a table, with default settings. + ** e.g., name & table name from the tableName parameter; type=TABLE; and LABEL_ONLY format + *******************************************************************************/ + public static QPossibleValueSource newForTable(String tableName) + { + return new QPossibleValueSource() + .withName(tableName) + .withType(QPossibleValueSourceType.TABLE) + .withTableName(tableName) + .withValueFormatAndFields(PVSValueFormatAndFields.LABEL_ONLY); + } + + + /******************************************************************************* ** *******************************************************************************/ 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 4f29ac04..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 @@ -61,6 +64,8 @@ public class QProcessMetaData implements QAppChildMetaData, MetaDataWithPermissi private QScheduleMetaData schedule; + private Map supplementalMetaData; + /******************************************************************************* @@ -544,4 +549,126 @@ public class QProcessMetaData implements QAppChildMetaData, MetaDataWithPermissi qInstance.addProcess(this); } + + + /******************************************************************************* + ** Getter for supplementalMetaData + *******************************************************************************/ + public Map getSupplementalMetaData() + { + return (this.supplementalMetaData); + } + + + + /******************************************************************************* + ** Getter for supplementalMetaData + *******************************************************************************/ + public QSupplementalProcessMetaData getSupplementalMetaData(String type) + { + if(this.supplementalMetaData == null) + { + return (null); + } + return this.supplementalMetaData.get(type); + } + + + + /******************************************************************************* + ** Setter for supplementalMetaData + *******************************************************************************/ + public void setSupplementalMetaData(Map supplementalMetaData) + { + this.supplementalMetaData = supplementalMetaData; + } + + + + /******************************************************************************* + ** Fluent setter for supplementalMetaData + *******************************************************************************/ + public QProcessMetaData withSupplementalMetaData(Map supplementalMetaData) + { + this.supplementalMetaData = supplementalMetaData; + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for supplementalMetaData + *******************************************************************************/ + public QProcessMetaData withSupplementalMetaData(QSupplementalProcessMetaData supplementalMetaData) + { + if(this.supplementalMetaData == null) + { + this.supplementalMetaData = new HashMap<>(); + } + this.supplementalMetaData.put(supplementalMetaData.getType(), supplementalMetaData); + 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 new file mode 100644 index 00000000..0a5dce8b --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QSupplementalProcessMetaData.java @@ -0,0 +1,92 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.model.metadata.processes; + + +import com.kingsrook.qqq.backend.core.instances.QInstanceEnricher; +import com.kingsrook.qqq.backend.core.instances.QInstanceValidator; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; + + +/******************************************************************************* + ** Base-class for process-level meta-data defined by some supplemental module, etc, + ** outside of qqq core + *******************************************************************************/ +public abstract class QSupplementalProcessMetaData +{ + protected String type; + + + + /******************************************************************************* + ** Getter for type + *******************************************************************************/ + public String getType() + { + return (this.type); + } + + + + /******************************************************************************* + ** Setter for type + *******************************************************************************/ + public void setType(String type) + { + this.type = type; + } + + + + /******************************************************************************* + ** Fluent setter for type + *******************************************************************************/ + public QSupplementalProcessMetaData withType(String type) + { + this.type = type; + return (this); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public void enrich(QInstanceEnricher qInstanceEnricher, QProcessMetaData process) + { + //////////////////////// + // noop in base class // + //////////////////////// + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public void validate(QInstance qInstance, QProcessMetaData process, QInstanceValidator qInstanceValidator) + { + //////////////////////// + // noop in base class // + //////////////////////// + } +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QMiddlewareTableMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QSupplementalTableMetaData.java similarity index 91% rename from qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QMiddlewareTableMetaData.java rename to qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QSupplementalTableMetaData.java index f6707861..1fb759db 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QMiddlewareTableMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QSupplementalTableMetaData.java @@ -23,9 +23,10 @@ package com.kingsrook.qqq.backend.core.model.metadata.tables; /******************************************************************************* - ** Base-class for table-level meta-data defined for a specific middleware. + ** Base-class for table-level meta-data defined by some supplemental module, etc, + ** outside of qqq core *******************************************************************************/ -public abstract class QMiddlewareTableMetaData +public abstract class QSupplementalTableMetaData { protected String type; @@ -54,7 +55,7 @@ public abstract class QMiddlewareTableMetaData /******************************************************************************* ** Fluent setter for type *******************************************************************************/ - public QMiddlewareTableMetaData withType(String type) + public QSupplementalTableMetaData withType(String type) { this.type = type; return (this); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QTableMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QTableMetaData.java index 76a93b41..508b2b38 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QTableMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QTableMetaData.java @@ -99,7 +99,7 @@ public class QTableMetaData implements QAppChildMetaData, Serializable, MetaData private CacheOf cacheOf; - private Map middlewareMetaData; + private Map supplementalMetaData; private List exposedJoins; @@ -1203,60 +1203,60 @@ public class QTableMetaData implements QAppChildMetaData, Serializable, MetaData /******************************************************************************* - ** Getter for middlewareMetaData + ** Getter for supplementalMetaData *******************************************************************************/ - public Map getMiddlewareMetaData() + public Map getSupplementalMetaData() { - return (this.middlewareMetaData); + return (this.supplementalMetaData); } /******************************************************************************* - ** Getter for middlewareMetaData + ** Getter for supplementalMetaData *******************************************************************************/ - public QMiddlewareTableMetaData getMiddlewareMetaData(String type) + public QSupplementalTableMetaData getSupplementalMetaData(String type) { - if(this.middlewareMetaData == null) + if(this.supplementalMetaData == null) { return (null); } - return this.middlewareMetaData.get(type); + return this.supplementalMetaData.get(type); } /******************************************************************************* - ** Setter for middlewareMetaData + ** Setter for supplementalMetaData *******************************************************************************/ - public void setMiddlewareMetaData(Map middlewareMetaData) + public void setSupplementalMetaData(Map supplementalMetaData) { - this.middlewareMetaData = middlewareMetaData; + this.supplementalMetaData = supplementalMetaData; } /******************************************************************************* - ** Fluent setter for middlewareMetaData + ** Fluent setter for supplementalMetaData *******************************************************************************/ - public QTableMetaData withMiddlewareMetaData(Map middlewareMetaData) + public QTableMetaData withSupplementalMetaData(Map supplementalMetaData) { - this.middlewareMetaData = middlewareMetaData; + this.supplementalMetaData = supplementalMetaData; return (this); } /******************************************************************************* - ** Fluent setter for middlewareMetaData + ** Fluent setter for supplementalMetaData *******************************************************************************/ - public QTableMetaData withMiddlewareMetaData(QMiddlewareTableMetaData middlewareMetaData) + public QTableMetaData withSupplementalMetaData(QSupplementalTableMetaData supplementalMetaData) { - if(this.middlewareMetaData == null) + if(this.supplementalMetaData == null) { - this.middlewareMetaData = new HashMap<>(); + this.supplementalMetaData = new HashMap<>(); } - this.middlewareMetaData.put(middlewareMetaData.getType(), middlewareMetaData); + this.supplementalMetaData.put(supplementalMetaData.getType(), supplementalMetaData); return (this); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedfilters/SavedFilter.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedfilters/SavedFilter.java new file mode 100644 index 00000000..9263b5d5 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedfilters/SavedFilter.java @@ -0,0 +1,283 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.model.savedfilters; + + +import java.time.Instant; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.data.QField; +import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.model.data.QRecordEntity; + + +/******************************************************************************* + ** Entity bean for the saved filter table + *******************************************************************************/ +public class SavedFilter extends QRecordEntity +{ + public static final String TABLE_NAME = "savedFilter"; + + @QField(isEditable = false) + private Integer id; + + @QField(isEditable = false) + private Instant createDate; + + @QField(isEditable = false) + private Instant modifyDate; + + @QField(isRequired = true) + private String label; + + @QField(isEditable = false) + private String tableName; + + @QField(isEditable = false) + private String userId; + + @QField(isEditable = false) + private String filterJson; + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public SavedFilter() + { + } + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public SavedFilter(QRecord qRecord) throws QException + { + populateFromQRecord(qRecord); + } + + + + /******************************************************************************* + ** Getter for id + ** + *******************************************************************************/ + public Integer getId() + { + return id; + } + + + + /******************************************************************************* + ** Setter for id + ** + *******************************************************************************/ + public void setId(Integer id) + { + this.id = id; + } + + + + /******************************************************************************* + ** Getter for createDate + ** + *******************************************************************************/ + public Instant getCreateDate() + { + return createDate; + } + + + + /******************************************************************************* + ** Setter for createDate + ** + *******************************************************************************/ + public void setCreateDate(Instant createDate) + { + this.createDate = createDate; + } + + + + /******************************************************************************* + ** Getter for modifyDate + ** + *******************************************************************************/ + public Instant getModifyDate() + { + return modifyDate; + } + + + + /******************************************************************************* + ** Setter for modifyDate + ** + *******************************************************************************/ + public void setModifyDate(Instant modifyDate) + { + this.modifyDate = modifyDate; + } + + + + /******************************************************************************* + ** Getter for label + ** + *******************************************************************************/ + public String getLabel() + { + return label; + } + + + + /******************************************************************************* + ** Setter for label + ** + *******************************************************************************/ + public void setLabel(String label) + { + this.label = label; + } + + + + /******************************************************************************* + ** Fluent setter for label + ** + *******************************************************************************/ + public SavedFilter withLabel(String label) + { + this.label = label; + return (this); + } + + + + /******************************************************************************* + ** Getter for tableName + ** + *******************************************************************************/ + public String getTableName() + { + return tableName; + } + + + + /******************************************************************************* + ** Setter for tableName + ** + *******************************************************************************/ + public void setTableName(String tableName) + { + this.tableName = tableName; + } + + + + /******************************************************************************* + ** Fluent setter for tableName + ** + *******************************************************************************/ + public SavedFilter withTableName(String tableName) + { + this.tableName = tableName; + return (this); + } + + + + /******************************************************************************* + ** Getter for userId + ** + *******************************************************************************/ + public String getUserId() + { + return userId; + } + + + + /******************************************************************************* + ** Setter for userId + ** + *******************************************************************************/ + public void setUserId(String userId) + { + this.userId = userId; + } + + + + /******************************************************************************* + ** Fluent setter for userId + ** + *******************************************************************************/ + public SavedFilter withUserId(String userId) + { + this.userId = userId; + return (this); + } + + + + /******************************************************************************* + ** Getter for filterJson + ** + *******************************************************************************/ + public String getFilterJson() + { + return filterJson; + } + + + + /******************************************************************************* + ** Setter for filterJson + ** + *******************************************************************************/ + public void setFilterJson(String filterJson) + { + this.filterJson = filterJson; + } + + + + /******************************************************************************* + ** Fluent setter for filterJson + ** + *******************************************************************************/ + public SavedFilter withFilterJson(String filterJson) + { + this.filterJson = filterJson; + return (this); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedfilters/SavedFiltersMetaDataProvider.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedfilters/SavedFiltersMetaDataProvider.java new file mode 100644 index 00000000..cb03e071 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedfilters/SavedFiltersMetaDataProvider.java @@ -0,0 +1,94 @@ +/* + * 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.backend.core.model.savedfilters; + + +import java.util.function.Consumer; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.PVSValueFormatAndFields; +import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource; +import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.processes.implementations.savedfilters.DeleteSavedFilterProcess; +import com.kingsrook.qqq.backend.core.processes.implementations.savedfilters.QuerySavedFilterProcess; +import com.kingsrook.qqq.backend.core.processes.implementations.savedfilters.StoreSavedFilterProcess; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class SavedFiltersMetaDataProvider +{ + + + /******************************************************************************* + ** + *******************************************************************************/ + public void defineAll(QInstance instance, String backendName, Consumer backendDetailEnricher) throws QException + { + instance.addTable(defineSavedFilterTable(backendName, backendDetailEnricher)); + instance.addPossibleValueSource(defineSavedFilterPossibleValueSource()); + instance.addProcess(QuerySavedFilterProcess.getProcessMetaData()); + instance.addProcess(StoreSavedFilterProcess.getProcessMetaData()); + instance.addProcess(DeleteSavedFilterProcess.getProcessMetaData()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private QTableMetaData defineSavedFilterTable(String backendName, Consumer backendDetailEnricher) throws QException + { + QTableMetaData table = new QTableMetaData() + .withName(SavedFilter.TABLE_NAME) + .withLabel("Saved Filter") + .withRecordLabelFormat("%s") + .withRecordLabelFields("label") + .withBackendName(backendName) + .withPrimaryKeyField("id") + .withFieldsFromEntity(SavedFilter.class); + + if(backendDetailEnricher != null) + { + backendDetailEnricher.accept(table); + } + + return (table); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private QPossibleValueSource defineSavedFilterPossibleValueSource() + { + return new QPossibleValueSource() + .withName(SavedFilter.TABLE_NAME) + .withType(QPossibleValueSourceType.TABLE) + .withTableName(SavedFilter.TABLE_NAME) + .withValueFormatAndFields(PVSValueFormatAndFields.LABEL_ONLY); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scripts/ScriptsMetaDataProvider.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scripts/ScriptsMetaDataProvider.java index de3399b9..fd5ed077 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scripts/ScriptsMetaDataProvider.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scripts/ScriptsMetaDataProvider.java @@ -102,6 +102,8 @@ public class ScriptsMetaDataProvider { return (new QProcessMetaData() .withName(STORE_SCRIPT_REVISION_PROCESS_NAME) + .withTableName(Script.TABLE_NAME) + .withIsHidden(true) .withStepList(List.of( new QBackendStepMetaData() .withName("main") @@ -118,6 +120,8 @@ public class ScriptsMetaDataProvider { return (new QProcessMetaData() .withName(TEST_SCRIPT_PROCESS_NAME) + .withTableName(Script.TABLE_NAME) + .withIsHidden(true) .withStepList(List.of( new QBackendStepMetaData() .withName("main") @@ -327,7 +331,12 @@ public class ScriptsMetaDataProvider .withSection(new QFieldSection("dates", new QIcon().withName("calendar_month"), Tier.T3, List.of("createDate", "modifyDate"))); tableMetaData.getField("scriptId").withPossibleValueSourceFilter(new QQueryFilter( - new QFilterCriteria("scriptType.name", QCriteriaOperator.EQUALS, SCRIPT_TYPE_NAME_RECORD) + new QFilterCriteria("scriptType.name", QCriteriaOperator.EQUALS, SCRIPT_TYPE_NAME_RECORD), + new QFilterCriteria("script.tableName", QCriteriaOperator.EQUALS, "${input.tableName}") + )); + + tableMetaData.getField("filterId").withPossibleValueSourceFilter(new QQueryFilter( + new QFilterCriteria("tableName", QCriteriaOperator.EQUALS, "${input.tableName}") )); return tableMetaData; 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..cf405c4a --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/CouldNotFindQueryFilterForExtractStepException.java @@ -0,0 +1,40 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +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/StreamedETLPreviewStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLPreviewStep.java index 99045d3a..4921c9ce 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLPreviewStep.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLPreviewStep.java @@ -27,6 +27,7 @@ import java.util.List; import com.kingsrook.qqq.backend.core.actions.async.AsyncRecordPipeLoop; import com.kingsrook.qqq.backend.core.actions.processes.BackendStep; import com.kingsrook.qqq.backend.core.actions.reporting.RecordPipe; +import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput; @@ -71,11 +72,14 @@ public class StreamedETLPreviewStep extends BaseStreamedETLStep implements Backe return; } - if(runBackendStepInput.getFrontendStepBehavior() != null && runBackendStepInput.getFrontendStepBehavior().equals(RunProcessInput.FrontendStepBehavior.SKIP)) - { - LOG.debug("Skipping preview because frontend behavior is [" + RunProcessInput.FrontendStepBehavior.SKIP + "]."); - return; - } + ////////////////////////////// + // set up the extract steps // + ////////////////////////////// + AbstractExtractStep extractStep = getExtractStep(runBackendStepInput); + RecordPipe recordPipe = new RecordPipe(); + extractStep.setLimit(limit); + extractStep.setRecordPipe(recordPipe); + extractStep.preRun(runBackendStepInput, runBackendStepOutput); ///////////////////////////////////////////////////////////////// // if we're running inside an automation, then skip this step. // @@ -86,17 +90,26 @@ public class StreamedETLPreviewStep extends BaseStreamedETLStep implements Backe return; } - ////////////////////////////////////////// - // set up the extract & transform steps // - ////////////////////////////////////////// - AbstractExtractStep extractStep = getExtractStep(runBackendStepInput); - RecordPipe recordPipe = new RecordPipe(); - extractStep.setLimit(limit); - extractStep.setRecordPipe(recordPipe); - extractStep.preRun(runBackendStepInput, runBackendStepOutput); + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // if skipping frontend steps, skip this action - // + // but, if inside an (ideally, only async) API call, at least do the count, so status calls can get x of y status // + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + if(RunProcessInput.FrontendStepBehavior.SKIP.equals(runBackendStepInput.getFrontendStepBehavior())) + { + if(QContext.getQSession().getValue("apiVersion") != null) + { + countRecords(runBackendStepInput, runBackendStepOutput, extractStep); + } + + LOG.debug("Skipping preview because frontend behavior is [" + RunProcessInput.FrontendStepBehavior.SKIP + "]."); + return; + } countRecords(runBackendStepInput, runBackendStepOutput, extractStep); + ////////////////////////////// + // setup the transform step // + ////////////////////////////// AbstractTransformStep transformStep = getTransformStep(runBackendStepInput); transformStep.preRun(runBackendStepInput, runBackendStepOutput); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/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/processes/implementations/savedfilters/DeleteSavedFilterProcess.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedfilters/DeleteSavedFilterProcess.java new file mode 100644 index 00000000..a0a6f1f8 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedfilters/DeleteSavedFilterProcess.java @@ -0,0 +1,88 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.processes.implementations.savedfilters; + + +import java.util.List; +import com.kingsrook.qqq.backend.core.actions.ActionHelper; +import com.kingsrook.qqq.backend.core.actions.processes.BackendStep; +import com.kingsrook.qqq.backend.core.actions.tables.DeleteAction; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.logging.QLogger; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput; +import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; +import com.kingsrook.qqq.backend.core.model.savedfilters.SavedFilter; + + +/******************************************************************************* + ** Process used by the delete filter dialog + *******************************************************************************/ +public class DeleteSavedFilterProcess implements BackendStep +{ + private static final QLogger LOG = QLogger.getLogger(DeleteSavedFilterProcess.class); + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static QProcessMetaData getProcessMetaData() + { + return (new QProcessMetaData() + .withName("deleteSavedFilter") + .withStepList(List.of( + new QBackendStepMetaData() + .withCode(new QCodeReference(DeleteSavedFilterProcess.class)) + .withName("delete") + ))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException + { + ActionHelper.validateSession(runBackendStepInput); + + try + { + Integer savedFilterId = runBackendStepInput.getValueInteger("id"); + + DeleteInput input = new DeleteInput(); + input.setTableName(SavedFilter.TABLE_NAME); + input.setPrimaryKeys(List.of(savedFilterId)); + new DeleteAction().execute(input); + } + catch(Exception e) + { + LOG.warn("Error deleting saved filter", e); + throw (e); + } + } +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedfilters/QuerySavedFilterProcess.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedfilters/QuerySavedFilterProcess.java new file mode 100644 index 00000000..dc50ed17 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedfilters/QuerySavedFilterProcess.java @@ -0,0 +1,117 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.processes.implementations.savedfilters; + + +import java.io.Serializable; +import java.util.List; +import com.kingsrook.qqq.backend.core.actions.ActionHelper; +import com.kingsrook.qqq.backend.core.actions.processes.BackendStep; +import com.kingsrook.qqq.backend.core.actions.tables.GetAction; +import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.logging.QLogger; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput; +import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; +import com.kingsrook.qqq.backend.core.model.savedfilters.SavedFilter; + + +/******************************************************************************* + ** Process used by the saved filter dialogs + *******************************************************************************/ +public class QuerySavedFilterProcess implements BackendStep +{ + private static final QLogger LOG = QLogger.getLogger(QuerySavedFilterProcess.class); + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static QProcessMetaData getProcessMetaData() + { + return (new QProcessMetaData() + .withName("querySavedFilter") + .withStepList(List.of( + new QBackendStepMetaData() + .withCode(new QCodeReference(QuerySavedFilterProcess.class)) + .withName("query") + ))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException + { + ActionHelper.validateSession(runBackendStepInput); + + try + { + Integer savedFilterId = runBackendStepInput.getValueInteger("id"); + if(savedFilterId != null) + { + GetInput input = new GetInput(); + input.setTableName(SavedFilter.TABLE_NAME); + input.setPrimaryKey(savedFilterId); + + GetOutput output = new GetAction().execute(input); + runBackendStepOutput.addRecord(output.getRecord()); + runBackendStepOutput.addValue("savedFilter", output.getRecord()); + runBackendStepOutput.addValue("savedFilterList", (Serializable) List.of(output.getRecord())); + } + else + { + String tableName = runBackendStepInput.getValueString("tableName"); + + QueryInput input = new QueryInput(); + input.setTableName(SavedFilter.TABLE_NAME); + input.setFilter(new QQueryFilter() + .withCriteria(new QFilterCriteria("tableName", QCriteriaOperator.EQUALS, tableName)) + .withOrderBy(new QFilterOrderBy("label"))); + + QueryOutput output = new QueryAction().execute(input); + runBackendStepOutput.setRecords(output.getRecords()); + runBackendStepOutput.addValue("savedFilterList", (Serializable) output.getRecords()); + } + } + catch(Exception e) + { + LOG.warn("Error deleting saved filter", e); + throw (e); + } + } +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedfilters/StoreSavedFilterProcess.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedfilters/StoreSavedFilterProcess.java new file mode 100644 index 00000000..37bc167e --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedfilters/StoreSavedFilterProcess.java @@ -0,0 +1,117 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.processes.implementations.savedfilters; + + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import com.kingsrook.qqq.backend.core.actions.ActionHelper; +import com.kingsrook.qqq.backend.core.actions.processes.BackendStep; +import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; +import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.logging.QLogger; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput; +import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput; +import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateOutput; +import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; +import com.kingsrook.qqq.backend.core.model.savedfilters.SavedFilter; + + +/******************************************************************************* + ** Process used by the saved filter dialog + *******************************************************************************/ +public class StoreSavedFilterProcess implements BackendStep +{ + private static final QLogger LOG = QLogger.getLogger(StoreSavedFilterProcess.class); + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static QProcessMetaData getProcessMetaData() + { + return (new QProcessMetaData() + .withName("storeSavedFilter") + .withStepList(List.of( + new QBackendStepMetaData() + .withCode(new QCodeReference(StoreSavedFilterProcess.class)) + .withName("store") + ))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException + { + ActionHelper.validateSession(runBackendStepInput); + + try + { + QRecord qRecord = new QRecord() + .withValue("id", runBackendStepInput.getValueInteger("id")) + .withValue("label", runBackendStepInput.getValueString("label")) + .withValue("tableName", runBackendStepInput.getValueString("tableName")) + .withValue("filterJson", runBackendStepInput.getValueString("filterJson")) + .withValue("userId", runBackendStepInput.getSession().getUser().getIdReference()); + + List savedFilterList = new ArrayList<>(); + if(qRecord.getValueInteger("id") == null) + { + InsertInput input = new InsertInput(); + input.setTableName(SavedFilter.TABLE_NAME); + input.setRecords(List.of(qRecord)); + + InsertOutput output = new InsertAction().execute(input); + savedFilterList = output.getRecords(); + } + else + { + UpdateInput input = new UpdateInput(); + input.setTableName(SavedFilter.TABLE_NAME); + input.setRecords(List.of(qRecord)); + + UpdateOutput output = new UpdateAction().execute(input); + savedFilterList = output.getRecords(); + } + + runBackendStepOutput.addValue("savedFilterList", (Serializable) savedFilterList); + } + catch(Exception e) + { + LOG.warn("Error storing data saved filter", e); + throw (e); + } + } +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ExceptionUtils.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ExceptionUtils.java index c4d71714..ae935308 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ExceptionUtils.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ExceptionUtils.java @@ -22,7 +22,9 @@ package com.kingsrook.qqq.backend.core.utils; +import java.util.ArrayList; import java.util.HashSet; +import java.util.List; import java.util.Set; @@ -60,6 +62,45 @@ public class ExceptionUtils + /******************************************************************************* + ** Find a list of exceptions of the given class in an exception's caused-by chain. + ** Returns empty list if none found. + ** + *******************************************************************************/ + public static List getClassListFromRootChain(Throwable e, Class targetClass) + { + List throwableList = new ArrayList<>(); + if(targetClass.isInstance(e)) + { + throwableList.add(targetClass.cast(e)); + } + + /////////////////////////////////////////////////// + // iterate through the chain with a limit of 100 // + /////////////////////////////////////////////////// + int counter = 0; + while(counter++ < 100) + { + //////////////////////////////////////////////////////////////////////// + // look for the same class from the last throwable found of that type // + //////////////////////////////////////////////////////////////////////// + e = findClassInRootChain(e.getCause(), targetClass); + if(e == null) + { + break; + } + + //////////////////////////////////////////////////////////////////////// + // if we did not break, higher one must have been found, keep looking // + //////////////////////////////////////////////////////////////////////// + throwableList.add(targetClass.cast(e)); + } + + return (throwableList); + } + + + /******************************************************************************* ** Get the root exception in a caused-by-chain. ** @@ -88,4 +129,39 @@ public class ExceptionUtils return (root); } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static String concatenateMessagesFromChain(Exception exception) + { + if(exception == null) + { + return (null); + } + + List messages = new ArrayList<>(); + Throwable root = exception; + Set seen = new HashSet<>(); + + do + { + if(StringUtils.hasContent(root.getMessage())) + { + messages.add(root.getMessage()); + } + else + { + messages.add(root.getClass().getSimpleName()); + } + + seen.add(root); + root = root.getCause(); + } + while(root != null && !seen.contains(root)); + + return (StringUtils.join("; ", messages)); + } } diff --git a/qqq-backend-core/src/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/BaseTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/BaseTest.java index f436eb11..1cae9018 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/BaseTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/BaseTest.java @@ -26,6 +26,7 @@ import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.session.QSession; +import com.kingsrook.qqq.backend.core.model.session.QUser; import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryRecordStore; import com.kingsrook.qqq.backend.core.utils.TestUtils; import org.junit.jupiter.api.AfterEach; @@ -47,7 +48,10 @@ public class BaseTest @BeforeEach void baseBeforeEach() { - QContext.init(TestUtils.defineInstance(), new QSession()); + QContext.init(TestUtils.defineInstance(), new QSession() + .withUser(new QUser() + .withIdReference("001") + .withFullName("Anonymous"))); resetMemoryRecordStore(); } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricherTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricherTest.java index 4deb3559..2e3ff3ec 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricherTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricherTest.java @@ -197,6 +197,16 @@ class QInstanceEnricherTest extends BaseTest assertEquals("Something USA", QInstanceEnricher.nameToLabel("somethingUSA")); assertEquals("Number 1 Dad", QInstanceEnricher.nameToLabel("number1Dad")); assertEquals("Number 417 Dad", QInstanceEnricher.nameToLabel("number417Dad")); + + assertEquals("Default Wms System Id", QInstanceEnricher.nameToLabel("defaultWmsSystemId")); + QInstanceEnricher.addLabelMapping("\\bWms\\b", "WMS"); + assertEquals("Default WMS System Id", QInstanceEnricher.nameToLabel("defaultWmsSystemId")); + QInstanceEnricher.clearLabelMappings(); + + assertEquals("Api Client Id", QInstanceEnricher.nameToLabel("apiClientId")); + QInstanceEnricher.addLabelMapping("\\bApi\\b", "API"); + assertEquals("API Client Id", QInstanceEnricher.nameToLabel("apiClientId")); + QInstanceEnricher.clearLabelMappings(); } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/logging/QLoggerTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/logging/QLoggerTest.java new file mode 100644 index 00000000..1c28982c --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/logging/QLoggerTest.java @@ -0,0 +1,190 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.logging; + + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import com.kingsrook.qqq.backend.core.BaseTest; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.core.Appender; +import org.apache.logging.log4j.core.Core; +import org.apache.logging.log4j.core.Filter; +import org.apache.logging.log4j.core.Layout; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.appender.AbstractAppender; +import org.apache.logging.log4j.core.config.Property; +import org.apache.logging.log4j.core.config.plugins.Plugin; +import org.apache.logging.log4j.core.config.plugins.PluginFactory; +import org.apache.logging.log4j.core.filter.LevelRangeFilter; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; + + +/******************************************************************************* + ** Unit test for com.kingsrook.qqq.backend.core.logging.QLogger + ** + *******************************************************************************/ +@Disabled // disabled because could never get the custom appender class to receive logEvents that have their levels set (always null) +class QLoggerTest extends BaseTest +{ + private static final QLogger LOG = QLogger.getLogger(QLoggerTest.class); + private static final ListAppender listAppender = ListAppender.createAppender(); + + + + /******************************************************************************* + ** + *******************************************************************************/ + @BeforeEach + void beforeAll() throws Exception + { + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testDowngradingWarnings() throws Exception + { + org.apache.logging.log4j.core.Logger logger = (org.apache.logging.log4j.core.Logger) LogManager.getLogger(QLoggerTest.class); + logger.addAppender(listAppender); + listAppender.start(); + + try + { + try + { + try + { + try + { + throw (new QException("Some deepest exception...")); + } + catch(Exception e) + { + String warning = "Less deep warning"; + LOG.warn(warning, e); + throw (new QException(warning, e)); + } + } + catch(Exception e2) + { + String warning = "Middle warning"; + LOG.warn(warning, e2); + throw (new QException(warning, e2)); + } + } + catch(Exception e2) + { + String warning = "Last warning"; + LOG.warn(warning, e2); + throw (new QException(warning, e2)); + } + } + catch(Exception e3) + { + ///////////////////////// + // check results below // + ///////////////////////// + } + + assertThat(listAppender.getEventList()).isNotNull(); + assertThat(listAppender.getEventList().size()).isEqualTo(5); + int counter = 0; + for(LogEvent logEvent : listAppender.getEventList()) + { + if(counter == 0) + { + assertThat(logEvent.getLevel()).isEqualTo(Level.WARN); + } + else + { + assertThat(logEvent.getLevel()).isEqualTo(Level.INFO); + } + counter++; + } + } + + + + /******************************************************************************* + ** appender to add to logger to keep a list of log events + *******************************************************************************/ + @Plugin(name = "ListAppender", category = Core.CATEGORY_NAME, elementType = Appender.ELEMENT_TYPE) + public static class ListAppender extends AbstractAppender + { + private List eventList = new ArrayList<>(); + + + + /******************************************************************************* + ** + *******************************************************************************/ + protected ListAppender(final String name, final Filter filter, final Layout layout, final boolean ignoreExceptions, final Property[] properties) + { + super(name, filter, layout, ignoreExceptions, properties); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @PluginFactory + public static ListAppender createAppender() + { + LevelRangeFilter levelRangeFilter = LevelRangeFilter.createFilter(Level.TRACE, Level.ERROR, Filter.Result.ACCEPT, Filter.Result.ACCEPT); + // return (new ListAppender("ListApppender", levelRangeFilter, null, true, null)); + return (new ListAppender("ListApppender", null, null, true, null)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void append(LogEvent event) + { + eventList.add(event); + } + + + + /******************************************************************************* + ** Getter for eventList + *******************************************************************************/ + public List getEventList() + { + return (this.eventList); + } + } + +} diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/savedfilters/SavedFilterProcessTests.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/savedfilters/SavedFilterProcessTests.java new file mode 100644 index 00000000..d3c0bf5b --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/savedfilters/SavedFilterProcessTests.java @@ -0,0 +1,143 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.processes.implementations.savedfilters; + + +import java.util.List; +import com.kingsrook.qqq.backend.core.BaseTest; +import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction; +import com.kingsrook.qqq.backend.core.context.QContext; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; +import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.savedfilters.SavedFiltersMetaDataProvider; +import com.kingsrook.qqq.backend.core.utils.JsonUtils; +import com.kingsrook.qqq.backend.core.utils.TestUtils; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + + +/******************************************************************************* + ** Unit test for all saved filter processes + *******************************************************************************/ +class SavedFilterProcessTests extends BaseTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void test() throws QException + { + QInstance qInstance = QContext.getQInstance(); + new SavedFiltersMetaDataProvider().defineAll(qInstance, TestUtils.MEMORY_BACKEND_NAME, null); + String tableName = TestUtils.TABLE_NAME_PERSON_MEMORY; + + { + /////////////////////////////////////////// + // query - should be no filters to start // + /////////////////////////////////////////// + RunProcessInput runProcessInput = new RunProcessInput(); + runProcessInput.setProcessName(QuerySavedFilterProcess.getProcessMetaData().getName()); + runProcessInput.addValue("tableName", tableName); + RunProcessOutput runProcessOutput = new RunProcessAction().execute(runProcessInput); + assertEquals(0, ((List) runProcessOutput.getValues().get("savedFilterList")).size()); + } + + Integer savedFilterId; + { + //////////////////////// + // store a new filter // + //////////////////////// + RunProcessInput runProcessInput = new RunProcessInput(); + runProcessInput.setProcessName(StoreSavedFilterProcess.getProcessMetaData().getName()); + runProcessInput.addValue("label", "My Filter"); + runProcessInput.addValue("tableName", tableName); + runProcessInput.addValue("filterJson", JsonUtils.toJson(new QQueryFilter(new QFilterCriteria("id", QCriteriaOperator.EQUALS, 47)))); + RunProcessOutput runProcessOutput = new RunProcessAction().execute(runProcessInput); + List savedFilterList = (List) runProcessOutput.getValues().get("savedFilterList"); + assertEquals(1, savedFilterList.size()); + savedFilterId = savedFilterList.get(0).getValueInteger("id"); + assertNotNull(savedFilterId); + } + + { + //////////////////////////////////// + // query - should find our filter // + //////////////////////////////////// + RunProcessInput runProcessInput = new RunProcessInput(); + runProcessInput.setProcessName(QuerySavedFilterProcess.getProcessMetaData().getName()); + runProcessInput.addValue("tableName", tableName); + RunProcessOutput runProcessOutput = new RunProcessAction().execute(runProcessInput); + List savedFilterList = (List) runProcessOutput.getValues().get("savedFilterList"); + assertEquals(1, savedFilterList.size()); + assertEquals(1, savedFilterList.get(0).getValueInteger("id")); + assertEquals("My Filter", savedFilterList.get(0).getValueString("label")); + } + + { + /////////////////////// + // update our filter // + /////////////////////// + RunProcessInput runProcessInput = new RunProcessInput(); + runProcessInput.setProcessName(StoreSavedFilterProcess.getProcessMetaData().getName()); + runProcessInput.addValue("id", savedFilterId); + runProcessInput.addValue("label", "My Updated Filter"); + runProcessInput.addValue("tableName", tableName); + runProcessInput.addValue("filterJson", JsonUtils.toJson(new QQueryFilter(new QFilterCriteria("id", QCriteriaOperator.EQUALS, 47)))); + RunProcessOutput runProcessOutput = new RunProcessAction().execute(runProcessInput); + List savedFilterList = (List) runProcessOutput.getValues().get("savedFilterList"); + assertEquals(1, savedFilterList.size()); + assertEquals(1, savedFilterList.get(0).getValueInteger("id")); + assertEquals("My Updated Filter", savedFilterList.get(0).getValueString("label")); + } + + { + /////////////////////// + // delete our filter // + /////////////////////// + RunProcessInput runProcessInput = new RunProcessInput(); + runProcessInput.setProcessName(DeleteSavedFilterProcess.getProcessMetaData().getName()); + runProcessInput.addValue("id", savedFilterId); + RunProcessOutput runProcessOutput = new RunProcessAction().execute(runProcessInput); + } + + { + //////////////////////////////////////// + // query - should be no filters again // + //////////////////////////////////////// + RunProcessInput runProcessInput = new RunProcessInput(); + runProcessInput.setProcessName(QuerySavedFilterProcess.getProcessMetaData().getName()); + runProcessInput.addValue("tableName", tableName); + RunProcessOutput runProcessOutput = new RunProcessAction().execute(runProcessInput); + assertEquals(0, ((List) runProcessOutput.getValues().get("savedFilterList")).size()); + } + + } + +} \ No newline at end of file diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/ExceptionUtilsTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/ExceptionUtilsTest.java index c92d4fe5..4b68f28b 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/ExceptionUtilsTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/ExceptionUtilsTest.java @@ -26,6 +26,7 @@ import com.kingsrook.qqq.backend.core.BaseTest; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException; import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertSame; @@ -88,6 +89,33 @@ class ExceptionUtilsTest extends BaseTest + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testConcatenateMessagesFromChain() + { + assertNull(ExceptionUtils.concatenateMessagesFromChain(null)); + assertEquals("QException", ExceptionUtils.concatenateMessagesFromChain(new QException((String) null))); + assertEquals("QException", ExceptionUtils.concatenateMessagesFromChain(new QException(""))); + assertEquals("foo; bar", ExceptionUtils.concatenateMessagesFromChain(new QException("foo", new QException("bar")))); + assertEquals("foo; QException; bar", ExceptionUtils.concatenateMessagesFromChain(new QException("foo", new QException(null, new QException("bar"))))); + + MyException selfCaused = new MyException("selfCaused"); + selfCaused.setCause(selfCaused); + assertEquals("selfCaused", ExceptionUtils.concatenateMessagesFromChain(selfCaused)); + + MyException cycle1 = new MyException("cycle1"); + MyException cycle2 = new MyException("cycle2"); + cycle1.setCause(cycle2); + cycle2.setCause(cycle1); + + assertEquals("cycle1; cycle2", ExceptionUtils.concatenateMessagesFromChain(cycle1)); + assertEquals("cycle2; cycle1", ExceptionUtils.concatenateMessagesFromChain(cycle2)); + } + + + /******************************************************************************* ** Test exception class - lets you set the cause, easier to create a loop. *******************************************************************************/ @@ -97,6 +125,9 @@ class ExceptionUtilsTest extends BaseTest + /******************************************************************************* + ** + *******************************************************************************/ public MyException(String message) { super(message); @@ -104,6 +135,9 @@ class ExceptionUtilsTest extends BaseTest + /******************************************************************************* + ** + *******************************************************************************/ public MyException(Throwable cause) { super(cause); @@ -111,6 +145,9 @@ class ExceptionUtilsTest extends BaseTest + /******************************************************************************* + ** + *******************************************************************************/ public void setCause(Throwable cause) { myCause = cause; diff --git a/qqq-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-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSUpdateAction.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSUpdateAction.java index 26af479b..035794ee 100644 --- a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSUpdateAction.java +++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSUpdateAction.java @@ -248,7 +248,7 @@ public class RDBMSUpdateAction extends AbstractRDBMSAction implements UpdateInte private String writeUpdateSQLPrefix(QTableMetaData table, List fieldsBeingUpdated) { String columns = fieldsBeingUpdated.stream() - .map(f -> this.getColumnName(table.getField(f)) + " = ?") + .map(f -> escapeIdentifier(this.getColumnName(table.getField(f))) + " = ?") .collect(Collectors.joining(", ")); String tableName = escapeIdentifier(getTableName(table)); diff --git a/qqq-dev-tools/CURRENT-SNAPSHOT-VERSION b/qqq-dev-tools/CURRENT-SNAPSHOT-VERSION index a5510516..04a373ef 100644 --- a/qqq-dev-tools/CURRENT-SNAPSHOT-VERSION +++ b/qqq-dev-tools/CURRENT-SNAPSHOT-VERSION @@ -1 +1 @@ -0.15.0 +0.16.0 diff --git a/qqq-language-support-javascript/src/main/java/com/kingsrook/qqq/languages/javascript/QJavaScriptExecutor.java b/qqq-language-support-javascript/src/main/java/com/kingsrook/qqq/languages/javascript/QJavaScriptExecutor.java index 15f7ac98..7a708ba7 100644 --- a/qqq-language-support-javascript/src/main/java/com/kingsrook/qqq/languages/javascript/QJavaScriptExecutor.java +++ b/qqq-language-support-javascript/src/main/java/com/kingsrook/qqq/languages/javascript/QJavaScriptExecutor.java @@ -27,6 +27,10 @@ import javax.script.ScriptEngine; import javax.script.ScriptEngineManager; import javax.script.ScriptException; import java.io.Serializable; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; import java.util.Map; import com.kingsrook.qqq.backend.core.actions.scripts.QCodeExecutor; import com.kingsrook.qqq.backend.core.actions.scripts.logging.QCodeExecutionLoggerInterface; @@ -36,8 +40,10 @@ import com.kingsrook.qqq.backend.core.utils.ExceptionUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils; import org.apache.commons.lang.NotImplementedException; import org.openjdk.nashorn.api.scripting.NashornScriptEngineFactory; +import org.openjdk.nashorn.api.scripting.ScriptObjectMirror; import org.openjdk.nashorn.internal.runtime.ECMAException; import org.openjdk.nashorn.internal.runtime.ParserException; +import org.openjdk.nashorn.internal.runtime.Undefined; /******************************************************************************* @@ -59,6 +65,59 @@ public class QJavaScriptExecutor implements QCodeExecutor + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public Object convertObjectToJava(Object object) + { + if(object == null || object instanceof String || object instanceof Boolean || object instanceof Integer || object instanceof Long || object instanceof BigDecimal) + { + return (object); + } + else if(object instanceof Float f) + { + return (new BigDecimal(f)); + } + else if(object instanceof Double d) + { + return (new BigDecimal(d)); + } + else if(object instanceof Undefined) + { + //////////////////////////////////////////////////////////////////////////////////////////////////////////// + // well, we always said we wanted javascript to treat null & undefined the same way... here's our chance // + //////////////////////////////////////////////////////////////////////////////////////////////////////////// + return (null); + } + + if(object instanceof ScriptObjectMirror scriptObjectMirror) + { + if(scriptObjectMirror.isArray()) + { + List result = new ArrayList<>(); + for(String key : scriptObjectMirror.keySet()) + { + result.add(Integer.parseInt(key), convertObjectToJava(scriptObjectMirror.get(key))); + } + return (result); + } + else + { + Map result = new HashMap<>(); + for(String key : scriptObjectMirror.keySet()) + { + result.put(key, convertObjectToJava(scriptObjectMirror.get(key))); + } + return (result); + } + } + + return QCodeExecutor.super.convertObjectToJava(object); + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-language-support-javascript/src/test/java/com/kingsrook/qqq/languages/javascript/ExecuteCodeActionTest.java b/qqq-language-support-javascript/src/test/java/com/kingsrook/qqq/languages/javascript/ExecuteCodeActionTest.java index 3c1bf009..43338983 100644 --- a/qqq-language-support-javascript/src/test/java/com/kingsrook/qqq/languages/javascript/ExecuteCodeActionTest.java +++ b/qqq-language-support-javascript/src/test/java/com/kingsrook/qqq/languages/javascript/ExecuteCodeActionTest.java @@ -23,7 +23,12 @@ package com.kingsrook.qqq.languages.javascript; import java.io.Serializable; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import com.kingsrook.qqq.backend.core.actions.scripts.ExecuteCodeAction; +import com.kingsrook.qqq.backend.core.actions.scripts.QCodeExecutor; +import com.kingsrook.qqq.backend.core.actions.scripts.QCodeExecutorAware; import com.kingsrook.qqq.backend.core.exceptions.QCodeException; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeInput; @@ -31,6 +36,7 @@ import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeOutput; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType; +import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; @@ -241,10 +247,50 @@ class ExecuteCodeActionTest extends BaseTest + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testConvertObjectToJava() throws QException + { + TestQCodeExecutorAware converter = new TestQCodeExecutorAware(); + testOne(1, """ + converter.convertObject("one", 1); + converter.convertObject("two", "two"); + converter.convertObject("true", true); + converter.convertObject("null", null); + converter.convertObject("undefined", undefined); + converter.convertObject("flatMap", {"a": 1, "b": "c"}); + converter.convertObject("flatList", ["a", 1, "b", "c"]); + converter.convertObject("mixedMap", {"a": [1, {"2": "3"}], "b": {"c": ["d"]}}); + """, MapBuilder.of("converter", converter)); + + assertEquals(1, converter.getConvertedObject("one")); + assertEquals("two", converter.getConvertedObject("two")); + assertEquals(true, converter.getConvertedObject("true")); + assertNull(converter.getConvertedObject("null")); + assertNull(converter.getConvertedObject("undefined")); + assertEquals(Map.of("a", 1, "b", "c"), converter.getConvertedObject("flatMap")); + assertEquals(List.of("a", 1, "b", "c"), converter.getConvertedObject("flatList")); + assertEquals(Map.of("a", List.of(1, Map.of("2", "3")), "b", Map.of("c", List.of("d"))), converter.getConvertedObject("mixedMap")); + } + + + /******************************************************************************* ** *******************************************************************************/ private OneTestOutput testOne(Integer inputValueC, String code) throws QException + { + return (testOne(inputValueC, code, null)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private OneTestOutput testOne(Integer inputValueC, String code, Map additionalContext) throws QException { System.out.println(); QInstance instance = TestUtils.defineInstance(); @@ -259,6 +305,14 @@ class ExecuteCodeActionTest extends BaseTest input.withContext("input", testInput); input.withContext("output", testOutput); + if(additionalContext != null) + { + for(Map.Entry entry : additionalContext.entrySet()) + { + input.withContext(entry.getKey(), entry.getValue()); + } + } + ExecuteCodeOutput output = new ExecuteCodeOutput(); ExecuteCodeAction executeCodeAction = new ExecuteCodeAction(); @@ -269,6 +323,49 @@ class ExecuteCodeActionTest extends BaseTest + /******************************************************************************* + ** + *******************************************************************************/ + public static class TestQCodeExecutorAware implements QCodeExecutorAware, Serializable + { + private QCodeExecutor qCodeExecutor; + + private Map convertedObjectMap = new HashMap<>(); + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void setQCodeExecutor(QCodeExecutor qCodeExecutor) + { + this.qCodeExecutor = qCodeExecutor; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public void convertObject(String name, Object inputObject) + { + convertedObjectMap.put(name, qCodeExecutor.convertObjectToJava(inputObject)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public Object getConvertedObject(String name) + { + return (convertedObjectMap.get(name)); + } + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/ApiMiddlewareType.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/ApiSupplementType.java similarity index 97% rename from qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/ApiMiddlewareType.java rename to qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/ApiSupplementType.java index 800ca283..cefaa68b 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/ApiMiddlewareType.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/ApiSupplementType.java @@ -25,7 +25,7 @@ package com.kingsrook.qqq.api; /******************************************************************************* ** *******************************************************************************/ -public interface ApiMiddlewareType +public interface ApiSupplementType { String NAME = "api"; 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 96660f60..315d9ed4 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/ApiImplementation.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/ApiImplementation.java @@ -25,19 +25,39 @@ package com.kingsrook.qqq.api.actions; import java.io.Serializable; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; +import java.util.UUID; +import java.util.concurrent.TimeUnit; import com.kingsrook.qqq.api.javalin.QBadRequestException; import com.kingsrook.qqq.api.model.APIVersion; +import com.kingsrook.qqq.api.model.actions.HttpApiResponse; import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaData; import com.kingsrook.qqq.api.model.metadata.ApiOperation; +import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessCustomizers; +import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessInput; +import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessInputFieldsContainer; +import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessMetaData; +import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessOutputInterface; +import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessUtils; +import com.kingsrook.qqq.api.model.metadata.processes.PostRunApiProcessCustomizer; +import com.kingsrook.qqq.api.model.metadata.processes.PreRunApiProcessCustomizer; import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaData; import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaDataContainer; +import com.kingsrook.qqq.backend.core.actions.async.AsyncJobManager; +import com.kingsrook.qqq.backend.core.actions.async.AsyncJobState; +import com.kingsrook.qqq.backend.core.actions.async.AsyncJobStatus; +import com.kingsrook.qqq.backend.core.actions.async.JobGoingAsyncException; +import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader; import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper; import com.kingsrook.qqq.backend.core.actions.permissions.TablePermissionSubType; +import com.kingsrook.qqq.backend.core.actions.processes.QProcessCallback; +import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction; import com.kingsrook.qqq.backend.core.actions.tables.CountAction; import com.kingsrook.qqq.backend.core.actions.tables.DeleteAction; import com.kingsrook.qqq.backend.core.actions.tables.GetAction; @@ -49,6 +69,9 @@ import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QNotFoundException; import com.kingsrook.qqq.backend.core.logging.LogPair; import com.kingsrook.qqq.backend.core.logging.QLogger; +import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessState; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput; import com.kingsrook.qqq.backend.core.model.actions.tables.QInputSource; import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput; import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput; @@ -67,8 +90,10 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput; import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput; import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateOutput; import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.statusmessages.BadInputStatusMessage; import com.kingsrook.qqq.backend.core.model.statusmessages.NotFoundStatusMessage; @@ -76,7 +101,9 @@ import com.kingsrook.qqq.backend.core.model.statusmessages.PermissionDeniedMessa import com.kingsrook.qqq.backend.core.model.statusmessages.QErrorMessage; import com.kingsrook.qqq.backend.core.model.statusmessages.QStatusMessage; import com.kingsrook.qqq.backend.core.model.statusmessages.QWarningMessage; +import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.CouldNotFindQueryFilterForExtractStepException; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import com.kingsrook.qqq.backend.core.utils.ExceptionUtils; import com.kingsrook.qqq.backend.core.utils.Pair; import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.core.utils.ValueUtils; @@ -87,6 +114,7 @@ import org.json.JSONArray; import org.json.JSONObject; import org.json.JSONTokener; import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; +import static com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator.IN; /******************************************************************************* @@ -96,9 +124,9 @@ public class ApiImplementation { private static final QLogger LOG = QLogger.getLogger(ApiImplementation.class); - ///////////////////////////////////// - // key: Pair // - ///////////////////////////////////// + /////////////////////////////////////////////////////////////////// + // key: Pair, value: Map metaData> // + /////////////////////////////////////////////////////////////////// private static Map, Map> tableApiNameMap = new HashMap<>(); @@ -286,7 +314,7 @@ public class ApiImplementation } else { - throw (new QBadRequestException("Request failed with " + badRequestMessages.size() + " reasons: " + StringUtils.join(" \n", badRequestMessages))); + throw (new QBadRequestException("Request failed with " + badRequestMessages.size() + " reasons: " + StringUtils.join("\n", badRequestMessages))); } } @@ -896,6 +924,291 @@ public class ApiImplementation + /******************************************************************************* + ** + *******************************************************************************/ + public static HttpApiResponse runProcess(ApiInstanceMetaData apiInstanceMetaData, String version, String processApiName, Map paramMap) throws QException + { + Pair pair = ApiProcessUtils.getProcessMetaDataPair(apiInstanceMetaData, version, processApiName); + + ApiProcessMetaData apiProcessMetaData = pair.getA(); + QProcessMetaData process = pair.getB(); + String processName = process.getName(); + + List badRequestMessages = new ArrayList<>(); + + String processUUID = UUID.randomUUID().toString(); + + RunProcessInput runProcessInput = new RunProcessInput(); + runProcessInput.setProcessName(processName); + runProcessInput.setFrontendStepBehavior(RunProcessInput.FrontendStepBehavior.SKIP); + runProcessInput.setProcessUUID(processUUID); + // todo i don't think runProcessInput.setAsyncJobCallback(); + + ////////////////////// + // map input values // + ////////////////////// + ApiProcessInput apiProcessInput = apiProcessMetaData.getInput(); + if(apiProcessInput != null) + { + processProcessInputFields(paramMap, badRequestMessages, runProcessInput, apiProcessInput.getQueryStringParams()); + processProcessInputFields(paramMap, badRequestMessages, runProcessInput, apiProcessInput.getFormParams()); + processProcessInputFields(paramMap, badRequestMessages, runProcessInput, apiProcessInput.getObjectBodyParams()); + + if(apiProcessInput.getBodyField() != null) + { + processSingleProcessInputField(apiProcessInput.getBodyField(), paramMap, badRequestMessages, runProcessInput); + } + } + + //////////////////////////////////////// + // get records for process, if needed // + //////////////////////////////////////// + // if(process.getMinInputRecords() != null && process.getMinInputRecords() > 0) + if(apiProcessInput != null && apiProcessInput.getRecordIdsParamName() != null) + { + String idParam = apiProcessInput.getRecordIdsParamName(); + if(StringUtils.hasContent(idParam) && StringUtils.hasContent(paramMap.get(idParam))) + { + String[] ids = paramMap.get(idParam).split(","); + + QTableMetaData table = QContext.getQInstance().getTable(process.getTableName()); + QQueryFilter filter = new QQueryFilter(new QFilterCriteria(table.getPrimaryKeyField(), IN, Arrays.asList(ids))); + runProcessInput.setCallback(getCallback(filter)); + } + } + + ///////////////////////////////////////// + // throw if bad inputs have been noted // + ///////////////////////////////////////// + if(!badRequestMessages.isEmpty()) + { + if(badRequestMessages.size() == 1) + { + throw (new QBadRequestException(badRequestMessages.get(0))); + } + else + { + throw (new QBadRequestException("Request failed with " + badRequestMessages.size() + " reasons: " + StringUtils.join("\n", badRequestMessages))); + } + } + + ///////////////////////////////////////// + // run pre-customizer, if there is one // + ///////////////////////////////////////// + Map customizers = apiProcessMetaData.getCustomizers(); + if(customizers != null && customizers.containsKey(ApiProcessCustomizers.PRE_RUN.getRole())) + { + PreRunApiProcessCustomizer preRunCustomizer = QCodeLoader.getAdHoc(PreRunApiProcessCustomizer.class, customizers.get(ApiProcessCustomizers.PRE_RUN.getRole())); + preRunCustomizer.preApiRun(runProcessInput); + } + + boolean async = false; + if(ApiProcessMetaData.AsyncMode.ALWAYS.equals(apiProcessMetaData.getAsyncMode()) + || (ApiProcessMetaData.AsyncMode.OPTIONAL.equals(apiProcessMetaData.getAsyncMode()) && "true".equalsIgnoreCase(paramMap.get("async")))) + { + async = true; + } + + if(async) + { + try + { + ////////////////////////////////////////////////////////////////////////////////////////////////////// + // note - in other implementations, the process gets its own UUID (for process state to be stashed) // + // and the job gets its own (where we check in on running/complete). // + // but in this implementation, we want to just pass back one UUID to the caller, so make the job // + // manager use the process's uuid as the job uuid, and all will be revealed! // + ////////////////////////////////////////////////////////////////////////////////////////////////////// + // todo? to help w/ StreamedETLPreview "should i count?" runProcessInput.setIsAsync(true); + new AsyncJobManager().withForcedJobUUID(processUUID).startJob(processName, 0, TimeUnit.MILLISECONDS, (callback) -> + { + runProcessInput.setAsyncJobCallback(callback); + return (new RunProcessAction().execute(runProcessInput)); + }); + } + catch(JobGoingAsyncException jgae) + { + LinkedHashMap response = new LinkedHashMap<>(); + response.put("jobId", jgae.getJobUUID()); + return (new HttpApiResponse(HttpStatus.Code.ACCEPTED, response)); + } + + //////////////////////////////////////////////////////////////////////////////////////// + // passing 0 as the timeout to startJob *should* make it always throw the JGAE. But, // + // in case it didn't, we don't have a uuid to return to the caller, so that's a fail. // + //////////////////////////////////////////////////////////////////////////////////////// + throw (new QException("Error starting asynchronous job - no job id was returned.")); + } + + ///////////////////// + // run the process // + ///////////////////// + RunProcessOutput runProcessOutput; + + try + { + RunProcessAction runProcessAction = new RunProcessAction(); + runProcessOutput = runProcessAction.execute(runProcessInput); + } + catch(CouldNotFindQueryFilterForExtractStepException e) + { + throw (new QBadRequestException("Records to run through this process were not specified.")); + } + catch(Exception e) + { + String concatenation = ExceptionUtils.concatenateMessagesFromChain(e); + throw (new QException(concatenation, e)); + } + + return (buildResponseAfterProcess(apiProcessMetaData, runProcessInput, runProcessOutput)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static HttpApiResponse buildResponseAfterProcess(ApiProcessMetaData apiProcessMetaData, RunProcessInput runProcessInput, RunProcessOutput runProcessOutput) throws QException + { + ////////////////////////////////////////// + // run post-customizer, if there is one // + ////////////////////////////////////////// + Map customizers = apiProcessMetaData.getCustomizers(); + if(customizers != null && customizers.containsKey(ApiProcessCustomizers.POST_RUN.getRole())) + { + PostRunApiProcessCustomizer postRunCustomizer = QCodeLoader.getAdHoc(PostRunApiProcessCustomizer.class, customizers.get(ApiProcessCustomizers.POST_RUN.getRole())); + postRunCustomizer.postApiRun(runProcessInput, runProcessOutput); + } + + /////////////////////// + // map output values // + /////////////////////// + ApiProcessOutputInterface output = apiProcessMetaData.getOutput(); + if(output != null) + { + return (new HttpApiResponse(output.getSuccessStatusCode(runProcessInput, runProcessOutput), output.getOutputForProcess(runProcessInput, runProcessOutput))); + } + else + { + return (new HttpApiResponse(HttpStatus.Code.NO_CONTENT, "")); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static void processProcessInputFields(Map paramMap, List badRequestMessages, RunProcessInput runProcessInput, ApiProcessInputFieldsContainer fieldsContainer) + { + if(fieldsContainer == null) + { + return; + } + + for(QFieldMetaData inputField : CollectionUtils.nonNullList(fieldsContainer.getFields())) + { + processSingleProcessInputField(inputField, paramMap, badRequestMessages, runProcessInput); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static void processSingleProcessInputField(QFieldMetaData inputField, Map paramMap, List badRequestMessages, RunProcessInput runProcessInput) + { + String value = paramMap.get(inputField.getName()); + + if(!StringUtils.hasContent(value) && inputField.getDefaultValue() != null) + { + value = ValueUtils.getValueAsString(inputField.getDefaultValue()); + } + + if(!StringUtils.hasContent(value) && inputField.getIsRequired()) + { + badRequestMessages.add("Missing value for required input field " + inputField.getName()); + return; + } + + // todo - types? + + runProcessInput.addValue(inputField.getName(), value); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static HttpApiResponse getProcessStatus(ApiInstanceMetaData apiInstanceMetaData, String version, String apiProcessName, String jobUUID) throws QException + { + Optional optionalJobStatus = new AsyncJobManager().getJobStatus(jobUUID); + if(optionalJobStatus.isEmpty()) + { + throw (new QException("Could not find status of process job: " + jobUUID)); + } + + AsyncJobStatus jobStatus = optionalJobStatus.get(); + + // resultForCaller.put("jobStatus", jobStatus); + LOG.debug("Job status is " + jobStatus.getState() + " for " + jobUUID); + + if(jobStatus.getState().equals(AsyncJobState.COMPLETE)) + { + /////////////////////////////////////////////////////////////////////////////////////// + // if the job is complete, get the process result from state provider, and return it // + // this output should look like it did if the job finished synchronously!! // + /////////////////////////////////////////////////////////////////////////////////////// + Optional processState = RunProcessAction.getState(jobUUID); + if(processState.isPresent()) + { + RunProcessOutput runProcessOutput = new RunProcessOutput(processState.get()); + RunProcessInput runProcessInput = new RunProcessInput(); + runProcessInput.seedFromProcessState(processState.get()); + + Pair pair = ApiProcessUtils.getProcessMetaDataPair(apiInstanceMetaData, version, apiProcessName); + + ApiProcessMetaData apiProcessMetaData = pair.getA(); + return (buildResponseAfterProcess(apiProcessMetaData, runProcessInput, runProcessOutput)); + } + else + { + throw (new QException("Could not find results for completed of process job: " + jobUUID)); + } + } + else if(jobStatus.getState().equals(AsyncJobState.ERROR)) + { + /////////////////////////////////////////////////////////////////////////////////////////////////////////// + // if the job had an error (e.g., a process step threw), "nicely" serialize its exception for the caller // + /////////////////////////////////////////////////////////////////////////////////////////////////////////// + if(jobStatus.getCaughtException() != null) + { + throw (new QException(jobStatus.getCaughtException())); + } + else + { + throw (new QException("Job failed with an unspecified error.")); + } + } + else + { + LinkedHashMap response = new LinkedHashMap<>(); + response.put("jobId", jobUUID); + response.put("message", jobStatus.getMessage()); + if(jobStatus.getCurrent() != null && jobStatus.getTotal() != null) + { + response.put("current", jobStatus.getCurrent()); + response.put("total", jobStatus.getTotal()); + } + return (new HttpApiResponse(HttpStatus.Code.ACCEPTED, response)); + } + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -1078,14 +1391,14 @@ public class ApiImplementation ApiTableMetaDataContainer apiTableMetaDataContainer = ApiTableMetaDataContainer.of(table); if(apiTableMetaDataContainer == null) { - LOG.info("404 because table apiMetaDataContainer is null", logPairs); + LOG.info("404 because table apiTableMetaDataContainer is null", logPairs); throw (new QNotFoundException("Could not find a table named " + tableApiName + " in this api.")); } ApiTableMetaData apiTableMetaData = apiTableMetaDataContainer.getApiTableMetaData(apiInstanceMetaData.getName()); if(apiTableMetaData == null) { - LOG.info("404 because table apiMetaData is null", logPairs); + LOG.info("404 because table apiTableMetaData is null", logPairs); throw (new QNotFoundException("Could not find a table named " + tableApiName + " in this api.")); } @@ -1185,4 +1498,29 @@ public class ApiImplementation return errors.stream().anyMatch(e -> (e instanceof NotFoundStatusMessage)); } + + + /******************************************************************************* + ** + *******************************************************************************/ + private static QProcessCallback getCallback(QQueryFilter filter) + { + return new QProcessCallback() + { + @Override + public QQueryFilter getQueryFilter() + { + return (filter); + } + + + + @Override + public Map 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..acb85f13 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,13 @@ import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaData; import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaDataContainer; import com.kingsrook.qqq.api.model.metadata.ApiOperation; import com.kingsrook.qqq.api.model.metadata.fields.ApiFieldMetaData; +import com.kingsrook.qqq.api.model.metadata.fields.ApiFieldMetaDataContainer; +import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessInput; +import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessInputFieldsContainer; +import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessMetaData; +import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessMetaDataContainer; +import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessOutputInterface; +import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessUtils; import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaData; import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaDataContainer; import com.kingsrook.qqq.api.model.openapi.Components; @@ -73,16 +80,19 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValue; import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource; import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.Association; import com.kingsrook.qqq.backend.core.model.metadata.tables.Capability; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.JsonUtils; import com.kingsrook.qqq.backend.core.utils.ObjectUtils; +import com.kingsrook.qqq.backend.core.utils.Pair; import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.core.utils.YamlUtils; import com.kingsrook.qqq.backend.core.utils.collections.ListBuilder; import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder; +import io.javalin.http.ContentType; import io.javalin.http.HttpStatus; import org.apache.commons.lang.BooleanUtils; @@ -217,7 +227,8 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction tagList = new ArrayList<>(); + Set usedProcessNames = new HashSet<>(); + + ///////////////////////////////////// + // foreach table (sorted by label) // + ///////////////////////////////////// List tables = new ArrayList<>(qInstance.getTables().values()); tables.sort(Comparator.comparing(t -> ObjectUtils.requireNonNullElse(t.getLabel(), t.getName(), ""))); for(QTableMetaData table : tables) @@ -330,7 +344,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction> apiProcessMetaDataList = getProcessesUnderTable(table, apiName, apiVersion); + + if(!getEnabled && !queryByQueryStringEnabled && !insertEnabled && !insertBulkEnabled && !updateEnabled && !updateBulkEnabled && !deleteEnabled && !deleteBulkEnabled && !CollectionUtils.nullSafeHasContents(apiProcessMetaDataList)) { - LOG.debug("Omitting table [" + tableName + "] because it does not have any supported capabilities / enabled operations"); + LOG.debug("Omitting table [" + tableName + "] because it does not have any supported capabilities / enabled operations or processes"); continue; } @@ -374,6 +390,10 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction tableApiFields = new GetTableApiFieldsAction().execute(new GetTableApiFieldsInput().withTableName(tableName).withVersion(version).withApiName(apiName)).getFields(); + tagList.add(new Tag() + .withName(tableLabel) + .withDescription("Operations on the " + tableLabel + " table.")); + /////////////////////////////// // permissions for the table // /////////////////////////////// @@ -405,13 +425,6 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction pair : CollectionUtils.nonNullList(apiProcessMetaDataList)) + { + ApiProcessMetaData apiProcessMetaData = pair.getA(); + QProcessMetaData processMetaData = pair.getB(); + + addProcessEndpoints(qInstance, apiInstanceMetaData, basePath, openAPI, tableProcessesTag, apiProcessMetaData, processMetaData); + + usedProcessNames.add(processMetaData.getName()); + } } + ///////////////////////////// + // add non-table processes // + ///////////////////////////// + if(input.getTableName() == null) + { + List> processesNotUnderTables = getProcessesNotUnderTables(apiName, apiVersion, usedProcessNames); + for(Pair pair : CollectionUtils.nonNullList(processesNotUnderTables)) + { + ApiProcessMetaData apiProcessMetaData = pair.getA(); + QProcessMetaData processMetaData = pair.getB(); + + String tag; + if(StringUtils.hasContent(apiProcessMetaData.getTag())) + { + tag = apiProcessMetaData.getTag(); + } + else + { + tag = processMetaData.getLabel(); + if(doesProcessLabelNeedTheWordProcessAppended(tag)) + { + tag += " process"; + } + } + + tagList.add(new Tag() + .withName(tag) + .withDescription(tag)); + + addProcessEndpoints(qInstance, apiInstanceMetaData, basePath, openAPI, tag, apiProcessMetaData, processMetaData); + + usedProcessNames.add(processMetaData.getName()); + } + } + + tagList.sort(Comparator.comparing(Tag::getName)); + openAPI.setTags(tagList); + + //////////////////////////// + // define standard errors // + //////////////////////////// componentResponses.put("error" + HttpStatus.BAD_REQUEST.getCode(), buildStandardErrorResponse("Bad Request. Some portion of the request's content was not acceptable to the server. See error message in body for details.", "Parameter id should be given an integer value, but received string: \"Foo\"")); componentResponses.put("error" + HttpStatus.UNAUTHORIZED.getCode(), buildStandardErrorResponse("Unauthorized. The required authentication credentials were missing or invalid.", "The required authentication credentials were missing or invalid.")); componentResponses.put("error" + HttpStatus.FORBIDDEN.getCode(), buildStandardErrorResponse("Forbidden. You do not have permission to access the requested resource.", "You do not have permission to access the requested resource.")); @@ -710,6 +789,372 @@ 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) + { + String description = apiProcessMetaData.getDescription(); + if(!StringUtils.hasContent(description)) + { + description = "Run the " + processMetaData.getLabel(); + if(doesProcessLabelNeedTheWordProcessAppended(description)) + { + description += " process"; + } + } + + //////////////////////////////// + // start defining the process // + //////////////////////////////// + Method methodForProcess = new Method() + .withOperationId(apiProcessMetaData.getApiProcessName()) + .withTags(tags) + .withSummary(ObjectUtils.requireConditionElse(apiProcessMetaData.getSummary(), StringUtils::hasContent, processMetaData.getLabel())) + .withDescription(description) + .withSecurity(getSecurity(apiInstanceMetaData, processMetaData.getName())); + + //////////////////////////////// + // add inputs for the process // + //////////////////////////////// + List parameters = new ArrayList<>(); + ApiProcessInput apiProcessInput = apiProcessMetaData.getInput(); + if(apiProcessInput != null) + { + ApiProcessInputFieldsContainer queryStringParams = apiProcessInput.getQueryStringParams(); + if(queryStringParams != null) + { + if(queryStringParams.getRecordIdsField() != null) + { + parameters.add(processFieldToParameter(apiInstanceMetaData, queryStringParams.getRecordIdsField()).withIn("query")); + } + + for(QFieldMetaData field : CollectionUtils.nonNullList(queryStringParams.getFields())) + { + parameters.add(processFieldToParameter(apiInstanceMetaData, field).withIn("query")); + } + } + + QFieldMetaData bodyField = apiProcessInput.getBodyField(); + if(bodyField != null) + { + ApiFieldMetaDataContainer apiFieldMetaDataContainer = ApiFieldMetaDataContainer.ofOrNew(bodyField); + ApiFieldMetaData apiFieldMetaData = apiFieldMetaDataContainer.getApiFieldMetaData(apiInstanceMetaData.getName()); + + String fieldLabel = bodyField.getLabel(); + if(!StringUtils.hasContent(fieldLabel)) + { + fieldLabel = QInstanceEnricher.nameToLabel(bodyField.getName()); + } + + String bodyDescription = "Value for the " + fieldLabel; + if(apiFieldMetaData != null && StringUtils.hasContent(apiFieldMetaData.getDescription())) + { + bodyDescription = apiFieldMetaData.getDescription(); + } + + Content content = new Content(); + if(apiFieldMetaData != null && apiFieldMetaData.getExample() instanceof ExampleWithSingleValue exampleWithSingleValue) + { + content.withSchema(new Schema() + .withDescription(bodyDescription) + .withType("string") + .withExample(exampleWithSingleValue.getValue()) + ); + } + + methodForProcess.withRequestBody(new RequestBody() + .withDescription(bodyDescription) + .withRequired(bodyField.getIsRequired()) + .withContent(MapBuilder.of(apiProcessInput.getBodyFieldContentType(), content))); + } + + // todo - form & record body params + // todo methodForProcess.withRequestBody(); + } + + //////////////////////////////////////////////////////// + // add the async input for optionally-async processes // + //////////////////////////////////////////////////////// + if(ApiProcessMetaData.AsyncMode.OPTIONAL.equals(apiProcessMetaData.getAsyncMode())) + { + parameters.add(new Parameter() + .withName("async") + .withIn("query") + .withDescription(""" + Indicates if the job should be ran asynchronously. + If false, or not specified, job is ran synchronously, and returns with response status of 207 (Multi-Status) or 204 (No Content). + If true, request returns immediately with response status of 202 (Accepted). + """) + .withExamples(MapBuilder.of( + "false", new ExampleWithSingleValue().withValue(false).withSummary("Run the job synchronously."), + "true", new ExampleWithSingleValue().withValue(true).withSummary("Run the job asynchronously.") + )) + .withSchema(new Schema().withType("boolean"))); + } + + if(CollectionUtils.nullSafeHasContents(parameters)) + { + methodForProcess.setParameters(parameters); + } + + ////////////////////////////////// + // build all possible responses // + ////////////////////////////////// + Map responses = new LinkedHashMap<>(); + + ApiProcessOutputInterface output = apiProcessMetaData.getOutput(); + if(!ApiProcessMetaData.AsyncMode.ALWAYS.equals(apiProcessMetaData.getAsyncMode())) + { + responses.putAll(output.getSpecResponses(apiInstanceMetaData.getName())); + } + if(!ApiProcessMetaData.AsyncMode.NEVER.equals(apiProcessMetaData.getAsyncMode())) + { + responses.put(HttpStatus.ACCEPTED.getCode(), new Response() + .withDescription("The process has been started asynchronously. You can call back later to check its status.") + .withContent(MapBuilder.of(ContentType.JSON, new Content() + .withSchema(new Schema() + .withType("object") + .withProperties(MapBuilder.of( + "jobId", new Schema().withType("string").withFormat("uuid").withDescription("id of the asynchronous job") + )) + ) + )) + ); + } + + responses.putAll(buildStandardErrorResponses(apiInstanceMetaData)); + methodForProcess.withResponses(responses); + + @SuppressWarnings("checkstyle:indentation") + Path path = switch(apiProcessMetaData.getMethod()) + { + case GET -> new Path().withGet(methodForProcess); + case POST -> new Path().withPost(methodForProcess); + case PUT -> new Path().withPut(methodForProcess); + case PATCH -> new Path().withPatch(methodForProcess); + case DELETE -> new Path().withDelete(methodForProcess); + }; + + return (path); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private Path generateProcessStatusSpecPathObject(ApiInstanceMetaData apiInstanceMetaData, ApiProcessMetaData apiProcessMetaData, QProcessMetaData processMetaData, List tags) + { + //////////////////////////////// + // start defining the process // + //////////////////////////////// + Method methodForProcess = new Method() + .withOperationId("getStatusFor" + StringUtils.ucFirst(apiProcessMetaData.getApiProcessName())) + .withTags(tags) + .withSummary("Get Status of Job: " + ObjectUtils.requireConditionElse(apiProcessMetaData.getSummary(), StringUtils::hasContent, processMetaData.getLabel())) + .withDescription("Get the status for a previous asynchronous call to the process named " + processMetaData.getLabel()) + .withSecurity(getSecurity(apiInstanceMetaData, processMetaData.getName())); + + //////////////////////////////////////////////////////// + // add the async input for optionally-async processes // + //////////////////////////////////////////////////////// + methodForProcess.setParameters(ListBuilder.of(new Parameter() + .withName("jobId") + .withIn("path") + .withRequired(true) + .withDescription("Id of the job, as returned by the API call that started it.") + .withSchema(new Schema().withType("string").withFormat("uuid")) + )); + + ////////////////////////////////// + // build all possible responses // + ////////////////////////////////// + Map responses = new LinkedHashMap<>(); + responses.put(HttpStatus.ACCEPTED.getCode(), new Response() + .withDescription("The process is still running. You can call back later to get its final status.") + .withContent(MapBuilder.of(ContentType.JSON, new Content() + .withSchema(new Schema() + .withType("object") + .withProperties(MapBuilder.of( + "jobId", new Schema().withType("string").withFormat("uuid").withDescription("id of the asynchronous job"), + "message", new Schema().withNullable(true).withType("string").withDescription("a status message about the progress of the job").withExample("Processing records"), + "current", new Schema().withNullable(true).withType("integer").withDescription("for jobs that count progress, indicator of the current number being processed").withExample(7), + "total", new Schema().withNullable(true).withType("integer").withDescription("for jobs that count progress, indicator of the total number being processed").withExample(9) + )) + ) + )) + ); + + ApiProcessOutputInterface output = apiProcessMetaData.getOutput(); + responses.putAll(output.getSpecResponses(apiInstanceMetaData.getName())); + responses.putAll(buildStandardErrorResponses(apiInstanceMetaData)); + + methodForProcess.withResponses(responses); + return (new Path().withGet(methodForProcess)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private Parameter processFieldToParameter(ApiInstanceMetaData apiInstanceMetaData, QFieldMetaData field) + { + ApiFieldMetaDataContainer apiFieldMetaDataContainer = ApiFieldMetaDataContainer.ofOrNew(field); + ApiFieldMetaData apiFieldMetaData = apiFieldMetaDataContainer.getApiFieldMetaData(apiInstanceMetaData.getName()); + + String fieldLabel = field.getLabel(); + if(!StringUtils.hasContent(fieldLabel)) + { + fieldLabel = QInstanceEnricher.nameToLabel(field.getName()); + } + + String description = "Value for the " + fieldLabel + " field."; + if(apiFieldMetaData != null && apiFieldMetaData.getDescription() != null) + { + description = apiFieldMetaData.getDescription(); + } + + if(field.getDefaultValue() != null) + { + description += " Default value is " + field.getDefaultValue() + ", if not given."; + } + + Schema fieldSchema = getFieldSchema(field, description, apiInstanceMetaData); + + Parameter parameter = new Parameter() + .withName(field.getName()) + .withDescription(description) + .withRequired(field.getIsRequired()) + .withSchema(fieldSchema); + + if(apiFieldMetaData != null) + { + if(apiFieldMetaData.getExample() != null) + { + parameter.withExample(apiFieldMetaData.getExample()); + } + else if(apiFieldMetaData.getExamples() != null) + { + parameter.withExamples(apiFieldMetaData.getExamples()); + } + } + + return (parameter); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private List> 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 @@ -766,7 +1211,14 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction apiLogUserIdCache = new HashMap<>(); @@ -156,10 +173,47 @@ public class QJavalinApiHandler //////////////////////////////////////////// ApiBuilder.get("/", context -> doSpecHtml(context, apiInstanceMetaData)); + /////////////////////////////////////////// + // add known paths for specs & docs page // + /////////////////////////////////////////// ApiBuilder.get("/openapi.yaml", context -> doSpecYaml(context, apiInstanceMetaData)); ApiBuilder.get("/openapi.json", context -> doSpecJson(context, apiInstanceMetaData)); ApiBuilder.get("/openapi.html", context -> doSpecHtml(context, apiInstanceMetaData)); + /////////////////// + // add processes // + /////////////////// + for(QProcessMetaData process : qInstance.getProcesses().values()) + { + ApiProcessMetaDataContainer apiProcessMetaDataContainer = Objects.requireNonNullElse(ApiProcessMetaDataContainer.of(process), EMPTY_API_PROCESS_META_DATA_CONTAINER); + ApiProcessMetaData apiProcessMetaData = apiProcessMetaDataContainer.getApis().get(apiInstanceMetaData.getName()); + + if(apiProcessMetaData != null && !BooleanUtils.isTrue(apiProcessMetaData.getIsExcluded())) + { + String path = ApiProcessUtils.getProcessApiPath(qInstance, process, apiProcessMetaData, apiInstanceMetaData); + HttpMethod method = apiProcessMetaData.getMethod(); + switch(method) + { + case GET -> ApiBuilder.get(path, context -> runProcess(context, process, apiProcessMetaData, apiInstanceMetaData)); + case POST -> ApiBuilder.post(path, context -> runProcess(context, process, apiProcessMetaData, apiInstanceMetaData)); + case PUT -> ApiBuilder.put(path, context -> runProcess(context, process, apiProcessMetaData, apiInstanceMetaData)); + case PATCH -> ApiBuilder.patch(path, context -> runProcess(context, process, apiProcessMetaData, apiInstanceMetaData)); + case DELETE -> ApiBuilder.delete(path, context -> runProcess(context, process, apiProcessMetaData, apiInstanceMetaData)); + default -> throw (new QRuntimeException("Unrecognized http method [" + method + "] for process [" + process.getName() + "]")); + } + + make405sForOtherMethods(method, path); + + if(!ApiProcessMetaData.AsyncMode.NEVER.equals(apiProcessMetaData.getAsyncMode())) + { + ApiBuilder.get(path + "/status/{jobId}", context -> getProcessStatus(context, process, apiProcessMetaData, apiInstanceMetaData)); + } + } + } + + /////////////////////////////////// + // add wildcard paths for tables // + /////////////////////////////////// ApiBuilder.path("/{tableName}", () -> { ApiBuilder.get("/openapi.yaml", context -> doSpecYaml(context, apiInstanceMetaData)); @@ -208,6 +262,171 @@ public class QJavalinApiHandler + /******************************************************************************* + ** + *******************************************************************************/ + private void make405sForOtherMethods(HttpMethod allowedMethod, String path) + { + if(!allowedMethod.equals(HttpMethod.GET)) + { + ApiBuilder.get(path, (Context c) -> QJavalinApiHandler.return405(c, allowedMethod)); + } + + if(!allowedMethod.equals(HttpMethod.POST)) + { + ApiBuilder.post(path, (Context c) -> QJavalinApiHandler.return405(c, allowedMethod)); + } + + if(!allowedMethod.equals(HttpMethod.PUT)) + { + ApiBuilder.put(path, (Context c) -> QJavalinApiHandler.return405(c, allowedMethod)); + } + + if(!allowedMethod.equals(HttpMethod.PATCH)) + { + ApiBuilder.patch(path, (Context c) -> QJavalinApiHandler.return405(c, allowedMethod)); + } + + if(!allowedMethod.equals(HttpMethod.DELETE)) + { + ApiBuilder.delete(path, (Context c) -> QJavalinApiHandler.return405(c, allowedMethod)); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static void return405(Context context, HttpMethod allowedMethod) + { + respondWithError(context, HttpStatus.Code.METHOD_NOT_ALLOWED, "This path only supports method: " + allowedMethod, newAPILog(context)); // 405 + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @SuppressWarnings("checkstyle:indentation") + private void runProcess(Context context, QProcessMetaData processMetaData, ApiProcessMetaData apiProcessMetaData, ApiInstanceMetaData apiInstanceMetaData) + { + String version = context.pathParam("version"); + APILog apiLog = newAPILog(context); + + try + { + setupSession(context, null, version, apiInstanceMetaData); + QJavalinAccessLogger.logStart("apiRunProcess", logPair("process", processMetaData.getName())); + + //////////////////////////////////////////////////// + // process inputs into map for api implementation // + //////////////////////////////////////////////////// + Map parameters = new LinkedHashMap<>(); + ApiProcessInput input = apiProcessMetaData.getInput(); + if(input != null) + { + processProcessInputFieldsContainer(context, parameters, input.getQueryStringParams(), Context::queryParam); + processProcessInputFieldsContainer(context, parameters, input.getFormParams(), Context::formParam); + + ApiProcessInputFieldsContainer objectBodyParams = input.getObjectBodyParams(); + if(objectBodyParams != null) + { + JSONObject jsonObject = new JSONObject(context.body()); + processProcessInputFieldsContainer(context, parameters, objectBodyParams, (ctx, name) -> jsonObject.optString(name, null)); + } + + if(input.getBodyField() != null) + { + parameters.put(input.getBodyField().getName(), context.body()); + } + } + + if(ApiProcessMetaData.AsyncMode.OPTIONAL.equals(apiProcessMetaData.getAsyncMode())) + { + parameters.put("async", context.queryParam("async")); + } + + ///////////////////// + // run the process // + ///////////////////// + HttpApiResponse response = ApiImplementation.runProcess(apiInstanceMetaData, version, apiProcessMetaData.getApiProcessName(), parameters); + + ////////////////// + // log & return // + ////////////////// + QJavalinAccessLogger.logEndSuccess(); + context.status(response.getStatusCode().getCode()); + String resultString = toJson(Objects.requireNonNullElse(response.getResponseBodyObject(), "")); + context.result(resultString); + storeApiLog(apiLog.withStatusCode(context.statusCode()).withResponseBody(resultString)); + } + catch(Exception e) + { + QJavalinAccessLogger.logEndFail(e); + handleException(context, e, apiLog); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private void getProcessStatus(Context context, QProcessMetaData processMetaData, ApiProcessMetaData apiProcessMetaData, ApiInstanceMetaData apiInstanceMetaData) + { + String version = context.pathParam("version"); + APILog apiLog = newAPILog(context); + + try + { + setupSession(context, null, version, apiInstanceMetaData); + QJavalinAccessLogger.logStart("apiGetProcessStatus", logPair("process", processMetaData.getName())); + + String jobId = context.pathParam("jobId"); + HttpApiResponse response = ApiImplementation.getProcessStatus(apiInstanceMetaData, version, apiProcessMetaData.getApiProcessName(), jobId); + + ////////////////// + // log & return // + ////////////////// + QJavalinAccessLogger.logEndSuccess(); + context.status(response.getStatusCode().getCode()); + String resultString = toJson(Objects.requireNonNullElse(response.getResponseBodyObject(), "")); + context.result(resultString); + storeApiLog(apiLog.withStatusCode(context.statusCode()).withResponseBody(resultString)); + } + catch(Exception e) + { + QJavalinAccessLogger.logEndFail(e); + handleException(context, e, apiLog); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static void processProcessInputFieldsContainer(Context context, Map parameters, ApiProcessInputFieldsContainer fieldsContainer, BiFunction paramAccessor) + { + 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); + } + } + } + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -1161,7 +1380,12 @@ public class QJavalinApiHandler // default exception handling // //////////////////////////////// LOG.warn("Exception in javalin request", e); - respondWithError(context, HttpStatus.Code.INTERNAL_SERVER_ERROR, e.getClass().getSimpleName() + " (" + e.getMessage() + ")", apiLog); // 500 + String message = e.getMessage(); + if(!StringUtils.hasContent(message)) + { + message = e.getClass().getSimpleName(); + } + respondWithError(context, HttpStatus.Code.INTERNAL_SERVER_ERROR, message, apiLog); // 500 return; } } 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/ApiInstanceMetaDataContainer.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/ApiInstanceMetaDataContainer.java index c26a9d39..cbb7aabd 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/ApiInstanceMetaDataContainer.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/ApiInstanceMetaDataContainer.java @@ -24,17 +24,17 @@ package com.kingsrook.qqq.api.model.metadata; import java.util.LinkedHashMap; import java.util.Map; -import com.kingsrook.qqq.api.ApiMiddlewareType; +import com.kingsrook.qqq.api.ApiSupplementType; import com.kingsrook.qqq.backend.core.instances.QInstanceValidator; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; -import com.kingsrook.qqq.backend.core.model.metadata.QMiddlewareInstanceMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.QSupplementalInstanceMetaData; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; /******************************************************************************* ** *******************************************************************************/ -public class ApiInstanceMetaDataContainer extends QMiddlewareInstanceMetaData +public class ApiInstanceMetaDataContainer extends QSupplementalInstanceMetaData { private Map apis; @@ -46,7 +46,7 @@ public class ApiInstanceMetaDataContainer extends QMiddlewareInstanceMetaData *******************************************************************************/ public ApiInstanceMetaDataContainer() { - setType(ApiMiddlewareType.NAME); + setType(ApiSupplementType.NAME); } @@ -56,7 +56,7 @@ public class ApiInstanceMetaDataContainer extends QMiddlewareInstanceMetaData *******************************************************************************/ public static ApiInstanceMetaDataContainer of(QInstance qInstance) { - return ((ApiInstanceMetaDataContainer) qInstance.getMiddlewareMetaData(ApiMiddlewareType.NAME)); + return ((ApiInstanceMetaDataContainer) qInstance.getSupplementalMetaData(ApiSupplementType.NAME)); } diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/fields/ApiFieldMetaData.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/fields/ApiFieldMetaData.java index fabc02f0..d03c7fa4 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/fields/ApiFieldMetaData.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/fields/ApiFieldMetaData.java @@ -22,6 +22,8 @@ package com.kingsrook.qqq.api.model.metadata.fields; +import java.util.Map; +import com.kingsrook.qqq.api.model.openapi.Example; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.utils.StringUtils; @@ -35,10 +37,14 @@ public class ApiFieldMetaData private String finalVersion; private String apiFieldName; + private String description; private Boolean isExcluded; private String replacedByFieldName; + private Example example; + private Map examples; + /******************************************************************************* @@ -214,4 +220,97 @@ public class ApiFieldMetaData return (this); } + + + /******************************************************************************* + ** Getter for description + *******************************************************************************/ + public String getDescription() + { + return (this.description); + } + + + + /******************************************************************************* + ** Setter for description + *******************************************************************************/ + public void setDescription(String description) + { + this.description = description; + } + + + + /******************************************************************************* + ** Fluent setter for description + *******************************************************************************/ + public ApiFieldMetaData withDescription(String description) + { + this.description = description; + return (this); + } + + + + /******************************************************************************* + ** Getter for example + *******************************************************************************/ + public Example getExample() + { + return (this.example); + } + + + + /******************************************************************************* + ** Setter for example + *******************************************************************************/ + public void setExample(Example example) + { + this.example = example; + } + + + + /******************************************************************************* + ** Fluent setter for example + *******************************************************************************/ + public ApiFieldMetaData withExample(Example example) + { + this.example = example; + return (this); + } + + + + /******************************************************************************* + ** Getter for examples + *******************************************************************************/ + public Map getExamples() + { + return (this.examples); + } + + + + /******************************************************************************* + ** Setter for examples + *******************************************************************************/ + public void setExamples(Map examples) + { + this.examples = examples; + } + + + + /******************************************************************************* + ** Fluent setter for examples + *******************************************************************************/ + public ApiFieldMetaData withExamples(Map examples) + { + this.examples = examples; + return (this); + } + } diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/fields/ApiFieldMetaDataContainer.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/fields/ApiFieldMetaDataContainer.java index d370b20f..fa9fe94a 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/fields/ApiFieldMetaDataContainer.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/fields/ApiFieldMetaDataContainer.java @@ -24,18 +24,20 @@ package com.kingsrook.qqq.api.model.metadata.fields; import java.util.LinkedHashMap; import java.util.Map; -import com.kingsrook.qqq.api.ApiMiddlewareType; +import java.util.Objects; +import com.kingsrook.qqq.api.ApiSupplementType; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; -import com.kingsrook.qqq.backend.core.model.metadata.fields.QMiddlewareFieldMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QSupplementalFieldMetaData; /******************************************************************************* ** *******************************************************************************/ -public class ApiFieldMetaDataContainer extends QMiddlewareFieldMetaData +public class ApiFieldMetaDataContainer extends QSupplementalFieldMetaData { private Map apis; + private ApiFieldMetaData defaultApiFieldMetaData; /******************************************************************************* @@ -54,7 +56,18 @@ public class ApiFieldMetaDataContainer extends QMiddlewareFieldMetaData *******************************************************************************/ public static ApiFieldMetaDataContainer of(QFieldMetaData field) { - return ((ApiFieldMetaDataContainer) field.getMiddlewareMetaData(ApiMiddlewareType.NAME)); + return ((ApiFieldMetaDataContainer) field.getSupplementalMetaData(ApiSupplementType.NAME)); + } + + + + /******************************************************************************* + ** either get the container attached to a field - or a new one - note - the new + ** one will NOT be attached to the field!! + *******************************************************************************/ + public static ApiFieldMetaDataContainer ofOrNew(QFieldMetaData field) + { + return (Objects.requireNonNullElseGet(of(field), ApiFieldMetaDataContainer::new)); } @@ -70,16 +83,16 @@ public class ApiFieldMetaDataContainer extends QMiddlewareFieldMetaData /******************************************************************************* - ** Getter for apis + ** Getter the apiFieldMetaData for a specific api, or the container's default *******************************************************************************/ public ApiFieldMetaData getApiFieldMetaData(String apiName) { if(this.apis == null) { - return (null); + return (defaultApiFieldMetaData); } - return (this.apis.get(apiName)); + return (this.apis.getOrDefault(apiName, defaultApiFieldMetaData)); } @@ -118,4 +131,35 @@ public class ApiFieldMetaDataContainer extends QMiddlewareFieldMetaData return (this); } + + + /******************************************************************************* + ** Getter for defaultApiFieldMetaData + *******************************************************************************/ + public ApiFieldMetaData getDefaultApiFieldMetaData() + { + return (this.defaultApiFieldMetaData); + } + + + + /******************************************************************************* + ** Setter for defaultApiFieldMetaData + *******************************************************************************/ + public void setDefaultApiFieldMetaData(ApiFieldMetaData defaultApiFieldMetaData) + { + this.defaultApiFieldMetaData = defaultApiFieldMetaData; + } + + + + /******************************************************************************* + ** Fluent setter for defaultApiFieldMetaData + *******************************************************************************/ + public ApiFieldMetaDataContainer withDefaultApiFieldMetaData(ApiFieldMetaData defaultApiFieldMetaData) + { + this.defaultApiFieldMetaData = defaultApiFieldMetaData; + return (this); + } + } diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessCustomizers.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessCustomizers.java new file mode 100644 index 00000000..daf01f93 --- /dev/null +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessCustomizers.java @@ -0,0 +1,87 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.api.model.metadata.processes; + + +/******************************************************************************* + ** + *******************************************************************************/ +public enum ApiProcessCustomizers +{ + PRE_RUN("preRun", PreRunApiProcessCustomizer.class), + POST_RUN("postRun", PreRunApiProcessCustomizer.class); + + private final String role; + private final Class expectedType; + + + + /******************************************************************************* + ** + *******************************************************************************/ + ApiProcessCustomizers(String role, Class expectedType) + { + this.role = role; + this.expectedType = expectedType; + } + + + + /******************************************************************************* + ** Get the FilesystemTableCustomer for a given role (e.g., the role used in meta-data, not + ** the enum-constant name). + *******************************************************************************/ + public static ApiProcessCustomizers forRole(String name) + { + for(ApiProcessCustomizers value : values()) + { + if(value.role.equals(name)) + { + return (value); + } + } + + return (null); + } + + + + /******************************************************************************* + ** Getter for role + ** + *******************************************************************************/ + public String getRole() + { + return role; + } + + + + /******************************************************************************* + ** Getter for expectedType + ** + *******************************************************************************/ + public Class getExpectedType() + { + return expectedType; + } +} 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..83b05691 --- /dev/null +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessInput.java @@ -0,0 +1,220 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.api.model.metadata.processes; + + +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class ApiProcessInput +{ + private ApiProcessInputFieldsContainer queryStringParams; + private ApiProcessInputFieldsContainer formParams; + private ApiProcessInputFieldsContainer recordBodyParams; + + private QFieldMetaData bodyField; + private String bodyFieldContentType; + + + + /******************************************************************************* + ** + *******************************************************************************/ + public String getRecordIdsParamName() + { + if(queryStringParams != null && queryStringParams.getRecordIdsField() != null) + { + return (queryStringParams.getRecordIdsField().getName()); + } + + if(formParams != null && formParams.getRecordIdsField() != null) + { + return (formParams.getRecordIdsField().getName()); + } + + if(recordBodyParams != null && recordBodyParams.getRecordIdsField() != null) + { + return (recordBodyParams.getRecordIdsField().getName()); + } + + return (null); + } + + + + /******************************************************************************* + ** Getter for queryStringParams + *******************************************************************************/ + public ApiProcessInputFieldsContainer getQueryStringParams() + { + return (this.queryStringParams); + } + + + + /******************************************************************************* + ** Setter for queryStringParams + *******************************************************************************/ + public void setQueryStringParams(ApiProcessInputFieldsContainer queryStringParams) + { + this.queryStringParams = queryStringParams; + } + + + + /******************************************************************************* + ** Fluent setter for queryStringParams + *******************************************************************************/ + public ApiProcessInput withQueryStringParams(ApiProcessInputFieldsContainer queryStringParams) + { + this.queryStringParams = queryStringParams; + return (this); + } + + + + /******************************************************************************* + ** Getter for formParams + *******************************************************************************/ + public ApiProcessInputFieldsContainer getFormParams() + { + return (this.formParams); + } + + + + /******************************************************************************* + ** Setter for formParams + *******************************************************************************/ + public void setFormParams(ApiProcessInputFieldsContainer formParams) + { + this.formParams = formParams; + } + + + + /******************************************************************************* + ** Fluent setter for formParams + *******************************************************************************/ + public ApiProcessInput withFormParams(ApiProcessInputFieldsContainer formParams) + { + this.formParams = formParams; + return (this); + } + + + + /******************************************************************************* + ** Getter for recordBodyParams + *******************************************************************************/ + public ApiProcessInputFieldsContainer getObjectBodyParams() + { + return (this.recordBodyParams); + } + + + + /******************************************************************************* + ** Setter for recordBodyParams + *******************************************************************************/ + public void setRecordBodyParams(ApiProcessInputFieldsContainer recordBodyParams) + { + this.recordBodyParams = recordBodyParams; + } + + + + /******************************************************************************* + ** Fluent setter for recordBodyParams + *******************************************************************************/ + public ApiProcessInput withRecordBodyParams(ApiProcessInputFieldsContainer recordBodyParams) + { + this.recordBodyParams = recordBodyParams; + return (this); + } + + + + /******************************************************************************* + ** Getter for bodyField + *******************************************************************************/ + public QFieldMetaData getBodyField() + { + return (this.bodyField); + } + + + + /******************************************************************************* + ** Setter for bodyField + *******************************************************************************/ + public void setBodyField(QFieldMetaData bodyField) + { + this.bodyField = bodyField; + } + + + + /******************************************************************************* + ** Fluent setter for bodyField + *******************************************************************************/ + public ApiProcessInput withBodyField(QFieldMetaData bodyField) + { + this.bodyField = bodyField; + return (this); + } + + + + /******************************************************************************* + ** Getter for bodyFieldContentType + *******************************************************************************/ + public String getBodyFieldContentType() + { + return (this.bodyFieldContentType); + } + + + + /******************************************************************************* + ** Setter for bodyFieldContentType + *******************************************************************************/ + public void setBodyFieldContentType(String bodyFieldContentType) + { + this.bodyFieldContentType = bodyFieldContentType; + } + + + + /******************************************************************************* + ** Fluent setter for bodyFieldContentType + *******************************************************************************/ + public ApiProcessInput withBodyFieldContentType(String bodyFieldContentType) + { + this.bodyFieldContentType = bodyFieldContentType; + return (this); + } + +} 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..67e2958f --- /dev/null +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessInputFieldsContainer.java @@ -0,0 +1,161 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.api.model.metadata.processes; + + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class ApiProcessInputFieldsContainer +{ + private QFieldMetaData recordIdsField; + private List fields; + + + + /******************************************************************************* + ** find all input fields in frontend steps of the process, and add them as fields + ** in this container. + *******************************************************************************/ + public ApiProcessInputFieldsContainer withInferredInputFields(QProcessMetaData processMetaData) + { + return (withInferredInputFieldsExcluding(processMetaData, Collections.emptySet())); + } + + + + /******************************************************************************* + ** find all input fields in frontend steps of the process, and add them as fields + ** in this container, unless they're in the collection to exclude. + *******************************************************************************/ + public ApiProcessInputFieldsContainer withInferredInputFieldsExcluding(QProcessMetaData processMetaData, Collection minusFieldNames) + { + if(fields == null) + { + fields = new ArrayList<>(); + } + + for(QStepMetaData stepMetaData : CollectionUtils.nonNullList(processMetaData.getStepList())) + { + if(stepMetaData instanceof QFrontendStepMetaData frontendStep) + { + for(QFieldMetaData inputField : frontendStep.getInputFields()) + { + if(minusFieldNames != null && !minusFieldNames.contains(inputField.getName())) + { + fields.add(inputField); + } + } + } + } + + return (this); + } + + + + /******************************************************************************* + ** Getter for recordIdsField + *******************************************************************************/ + public QFieldMetaData getRecordIdsField() + { + return (this.recordIdsField); + } + + + + /******************************************************************************* + ** Setter for recordIdsField + *******************************************************************************/ + public void setRecordIdsField(QFieldMetaData recordIdsField) + { + this.recordIdsField = recordIdsField; + } + + + + /******************************************************************************* + ** Fluent setter for recordIdsField + *******************************************************************************/ + public ApiProcessInputFieldsContainer withRecordIdsField(QFieldMetaData recordIdsField) + { + this.recordIdsField = recordIdsField; + return (this); + } + + + + /******************************************************************************* + ** Getter for fields + *******************************************************************************/ + public List 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 new file mode 100644 index 00000000..e1694dd7 --- /dev/null +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessMetaData.java @@ -0,0 +1,644 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.api.model.metadata.processes; + + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import com.kingsrook.qqq.api.ApiSupplementType; +import com.kingsrook.qqq.api.model.APIVersionRange; +import com.kingsrook.qqq.api.model.metadata.fields.ApiFieldMetaData; +import com.kingsrook.qqq.api.model.metadata.fields.ApiFieldMetaDataContainer; +import com.kingsrook.qqq.api.model.openapi.HttpMethod; +import com.kingsrook.qqq.backend.core.instances.QInstanceEnricher; +import com.kingsrook.qqq.backend.core.instances.QInstanceValidator; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import com.kingsrook.qqq.backend.core.utils.StringUtils; +import com.kingsrook.qqq.backend.core.utils.collections.ListBuilder; +import org.apache.commons.lang.BooleanUtils; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class ApiProcessMetaData +{ + private String initialVersion; + private String finalVersion; + + private String apiProcessName; + private Boolean isExcluded; + private Boolean overrideProcessIsHidden; + + private String path; + private HttpMethod method; + private String summary; + private String description; + private String tag; + + private AsyncMode asyncMode = AsyncMode.OPTIONAL; + + private ApiProcessInput input; + private ApiProcessOutputInterface output; + + private Map customizers; + + + + public enum AsyncMode + { + NEVER, + OPTIONAL, + ALWAYS + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public APIVersionRange getApiVersionRange() + { + if(getInitialVersion() == null) + { + return APIVersionRange.none(); + } + + return (getFinalVersion() != null + ? APIVersionRange.betweenAndIncluding(getInitialVersion(), getFinalVersion()) + : APIVersionRange.afterAndIncluding(getInitialVersion())); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public void enrich(QInstanceEnricher qInstanceEnricher, String apiName, QProcessMetaData process) + { + if(!StringUtils.hasContent(getApiProcessName())) + { + setApiProcessName(process.getName()); + } + + if(initialVersion != null) + { + if(getOutput() instanceof ApiProcessObjectOutput outputObject) + { + enrichFieldList(qInstanceEnricher, apiName, outputObject.getOutputFields()); + } + + if(input != null) + { + for(ApiProcessInputFieldsContainer fieldsContainer : ListBuilder.of(input.getQueryStringParams(), input.getFormParams(), input.getObjectBodyParams())) + { + if(fieldsContainer != null) + { + enrichFieldList(qInstanceEnricher, apiName, fieldsContainer.getFields()); + } + } + if(input.getBodyField() != null) + { + enrichFieldList(qInstanceEnricher, apiName, List.of(input.getBodyField())); + } + } + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private void enrichFieldList(QInstanceEnricher qInstanceEnricher, String apiName, List fields) + { + for(QFieldMetaData field : CollectionUtils.nonNullList(fields)) + { + ApiFieldMetaData apiFieldMetaData = ensureFieldHasApiSupplementalMetaData(apiName, field); + if(apiFieldMetaData.getInitialVersion() == null) + { + apiFieldMetaData.setInitialVersion(initialVersion); + } + + qInstanceEnricher.enrichField(field); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static ApiFieldMetaData ensureFieldHasApiSupplementalMetaData(String apiName, QFieldMetaData field) + { + if(field.getSupplementalMetaData(ApiSupplementType.NAME) == null) + { + field.withSupplementalMetaData(new ApiFieldMetaDataContainer()); + } + + ApiFieldMetaDataContainer apiFieldMetaDataContainer = ApiFieldMetaDataContainer.of(field); + if(apiFieldMetaDataContainer.getApiFieldMetaData(apiName) == null) + { + apiFieldMetaDataContainer.withApiFieldMetaData(apiName, new ApiFieldMetaData()); + } + + return (apiFieldMetaDataContainer.getApiFieldMetaData(apiName)); + } + + + + /******************************************************************************* + ** Getter for initialVersion + *******************************************************************************/ + public String getInitialVersion() + { + return (this.initialVersion); + } + + + + /******************************************************************************* + ** Setter for initialVersion + *******************************************************************************/ + public void setInitialVersion(String initialVersion) + { + this.initialVersion = initialVersion; + } + + + + /******************************************************************************* + ** Fluent setter for initialVersion + *******************************************************************************/ + public ApiProcessMetaData withInitialVersion(String initialVersion) + { + this.initialVersion = initialVersion; + return (this); + } + + + + /******************************************************************************* + ** Getter for finalVersion + *******************************************************************************/ + public String getFinalVersion() + { + return (this.finalVersion); + } + + + + /******************************************************************************* + ** Setter for finalVersion + *******************************************************************************/ + public void setFinalVersion(String finalVersion) + { + this.finalVersion = finalVersion; + } + + + + /******************************************************************************* + ** Fluent setter for finalVersion + *******************************************************************************/ + public ApiProcessMetaData withFinalVersion(String finalVersion) + { + this.finalVersion = finalVersion; + return (this); + } + + + + /******************************************************************************* + ** Getter for apiProcessName + *******************************************************************************/ + public String getApiProcessName() + { + return (this.apiProcessName); + } + + + + /******************************************************************************* + ** Setter for apiProcessName + *******************************************************************************/ + public void setApiProcessName(String apiProcessName) + { + this.apiProcessName = apiProcessName; + } + + + + /******************************************************************************* + ** Fluent setter for apiProcessName + *******************************************************************************/ + public ApiProcessMetaData withApiProcessName(String apiProcessName) + { + this.apiProcessName = apiProcessName; + return (this); + } + + + + /******************************************************************************* + ** Getter for isExcluded + *******************************************************************************/ + public Boolean getIsExcluded() + { + return (this.isExcluded); + } + + + + /******************************************************************************* + ** Setter for isExcluded + *******************************************************************************/ + public void setIsExcluded(Boolean isExcluded) + { + this.isExcluded = isExcluded; + } + + + + /******************************************************************************* + ** Fluent setter for isExcluded + *******************************************************************************/ + public ApiProcessMetaData withIsExcluded(Boolean isExcluded) + { + this.isExcluded = isExcluded; + return (this); + } + + + + /******************************************************************************* + ** Getter for method + *******************************************************************************/ + public HttpMethod getMethod() + { + return (this.method); + } + + + + /******************************************************************************* + ** Setter for method + *******************************************************************************/ + public void setMethod(HttpMethod method) + { + this.method = method; + } + + + + /******************************************************************************* + ** Fluent setter for method + *******************************************************************************/ + public ApiProcessMetaData withMethod(HttpMethod method) + { + this.method = method; + return (this); + } + + + + /******************************************************************************* + ** Getter for path + *******************************************************************************/ + public String getPath() + { + return (this.path); + } + + + + /******************************************************************************* + ** Setter for path + *******************************************************************************/ + public void setPath(String path) + { + this.path = path; + } + + + + /******************************************************************************* + ** Fluent setter for path + *******************************************************************************/ + public ApiProcessMetaData withPath(String path) + { + this.path = path; + return (this); + } + + + + /******************************************************************************* + ** Getter for customizers + *******************************************************************************/ + public Map getCustomizers() + { + return (this.customizers); + } + + + + /******************************************************************************* + ** Setter for customizers + *******************************************************************************/ + public void setCustomizers(Map customizers) + { + this.customizers = customizers; + } + + + + /******************************************************************************* + ** Fluent setter for customizers + *******************************************************************************/ + public ApiProcessMetaData withCustomizers(Map customizers) + { + this.customizers = customizers; + return (this); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public ApiProcessMetaData withCustomizer(String role, QCodeReference customizer) + { + if(this.customizers == null) + { + this.customizers = new HashMap<>(); + } + + if(this.customizers.containsKey(role)) + { + throw (new IllegalArgumentException("Attempt to add a second customizer with role [" + role + "] to apiProcess [" + apiProcessName + "].")); + } + this.customizers.put(role, customizer); + return (this); + } + + + + /******************************************************************************* + ** Getter for output + *******************************************************************************/ + public ApiProcessOutputInterface getOutput() + { + return (this.output); + } + + + + /******************************************************************************* + ** Setter for output + *******************************************************************************/ + public void setOutput(ApiProcessOutputInterface output) + { + this.output = output; + } + + + + /******************************************************************************* + ** Fluent setter for output + *******************************************************************************/ + public ApiProcessMetaData withOutput(ApiProcessOutputInterface output) + { + this.output = output; + return (this); + } + + + + /******************************************************************************* + ** Getter for input + *******************************************************************************/ + public ApiProcessInput getInput() + { + return (this.input); + } + + + + /******************************************************************************* + ** Setter for input + *******************************************************************************/ + public void setInput(ApiProcessInput input) + { + this.input = input; + } + + + + /******************************************************************************* + ** Fluent setter for input + *******************************************************************************/ + public ApiProcessMetaData withInput(ApiProcessInput input) + { + this.input = input; + return (this); + } + + + + /******************************************************************************* + ** Getter for summary + *******************************************************************************/ + public String getSummary() + { + return (this.summary); + } + + + + /******************************************************************************* + ** Setter for summary + *******************************************************************************/ + public void setSummary(String summary) + { + this.summary = summary; + } + + + + /******************************************************************************* + ** Fluent setter for summary + *******************************************************************************/ + public ApiProcessMetaData withSummary(String summary) + { + this.summary = summary; + return (this); + } + + + + /******************************************************************************* + ** Getter for description + *******************************************************************************/ + public String getDescription() + { + return (this.description); + } + + + + /******************************************************************************* + ** Setter for description + *******************************************************************************/ + public void setDescription(String description) + { + this.description = description; + } + + + + /******************************************************************************* + ** Fluent setter for description + *******************************************************************************/ + public ApiProcessMetaData withDescription(String description) + { + this.description = description; + return (this); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public void validate(QInstance qInstance, QProcessMetaData process, QInstanceValidator qInstanceValidator, String apiName) + { + if(BooleanUtils.isTrue(getIsExcluded())) + { + ///////////////////////////////////////////////// + // no validation needed for excluded processes // + ///////////////////////////////////////////////// + return; + } + + qInstanceValidator.assertCondition(getMethod() != null, "Missing a method for api process meta data for process: " + process.getName() + ", apiName: " + apiName); + } + + + + /******************************************************************************* + ** Getter for asyncMode + *******************************************************************************/ + public AsyncMode getAsyncMode() + { + return (this.asyncMode); + } + + + + /******************************************************************************* + ** Setter for asyncMode + *******************************************************************************/ + public void setAsyncMode(AsyncMode asyncMode) + { + this.asyncMode = asyncMode; + } + + + + /******************************************************************************* + ** Fluent setter for asyncMode + *******************************************************************************/ + public ApiProcessMetaData withAsyncMode(AsyncMode asyncMode) + { + this.asyncMode = asyncMode; + return (this); + } + + + + /******************************************************************************* + ** Getter for overrideProcessIsHidden + *******************************************************************************/ + public Boolean getOverrideProcessIsHidden() + { + return (this.overrideProcessIsHidden); + } + + + + /******************************************************************************* + ** Setter for overrideProcessIsHidden + *******************************************************************************/ + public void setOverrideProcessIsHidden(Boolean overrideProcessIsHidden) + { + this.overrideProcessIsHidden = overrideProcessIsHidden; + } + + + + /******************************************************************************* + ** Fluent setter for overrideProcessIsHidden + *******************************************************************************/ + public ApiProcessMetaData withOverrideProcessIsHidden(Boolean overrideProcessIsHidden) + { + this.overrideProcessIsHidden = overrideProcessIsHidden; + return (this); + } + + + + /******************************************************************************* + ** Getter for tag + *******************************************************************************/ + public String getTag() + { + return (this.tag); + } + + + + /******************************************************************************* + ** Setter for tag + *******************************************************************************/ + public void setTag(String tag) + { + this.tag = tag; + } + + + + /******************************************************************************* + ** Fluent setter for tag + *******************************************************************************/ + public ApiProcessMetaData withTag(String tag) + { + this.tag = tag; + 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 new file mode 100644 index 00000000..496e8ea0 --- /dev/null +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessMetaDataContainer.java @@ -0,0 +1,189 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.api.model.metadata.processes; + + +import java.util.LinkedHashMap; +import java.util.Map; +import com.kingsrook.qqq.api.ApiSupplementType; +import com.kingsrook.qqq.backend.core.instances.QInstanceEnricher; +import com.kingsrook.qqq.backend.core.instances.QInstanceValidator; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QSupplementalProcessMetaData; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class ApiProcessMetaDataContainer extends QSupplementalProcessMetaData +{ + private Map apis; + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public ApiProcessMetaDataContainer() + { + setType(ApiSupplementType.NAME); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static ApiProcessMetaDataContainer of(QProcessMetaData process) + { + return ((ApiProcessMetaDataContainer) process.getSupplementalMetaData(ApiSupplementType.NAME)); + } + + + + /******************************************************************************* + ** either get the container attached to a field - or create a new one and attach + ** it to the field, and return that. + *******************************************************************************/ + public static ApiProcessMetaDataContainer ofOrWithNew(QProcessMetaData process) + { + ApiProcessMetaDataContainer apiProcessMetaDataContainer = (ApiProcessMetaDataContainer) process.getSupplementalMetaData(ApiSupplementType.NAME); + if(apiProcessMetaDataContainer == null) + { + apiProcessMetaDataContainer = new ApiProcessMetaDataContainer(); + process.withSupplementalMetaData(apiProcessMetaDataContainer); + } + return (apiProcessMetaDataContainer); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void enrich(QInstanceEnricher qInstanceEnricher, QProcessMetaData process) + { + super.enrich(qInstanceEnricher, process); + + for(Map.Entry entry : CollectionUtils.nonNullMap(apis).entrySet()) + { + entry.getValue().enrich(qInstanceEnricher, entry.getKey(), process); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void validate(QInstance qInstance, QProcessMetaData process, QInstanceValidator qInstanceValidator) + { + super.validate(qInstance, process, qInstanceValidator); + + for(Map.Entry entry : CollectionUtils.nonNullMap(apis).entrySet()) + { + entry.getValue().validate(qInstance, process, qInstanceValidator, entry.getKey()); + } + } + + + + /******************************************************************************* + ** Getter for apis + *******************************************************************************/ + public Map getApis() + { + return (this.apis); + } + + + + /******************************************************************************* + ** Getter for apis + *******************************************************************************/ + public ApiProcessMetaData getApiProcessMetaData(String apiName) + { + if(this.apis == null) + { + return (null); + } + + return (this.apis.get(apiName)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public ApiProcessMetaData getApiProcessMetaDataOrWithNew(String apiName) + { + ApiProcessMetaData apiProcessMetaData = getApiProcessMetaData(apiName); + if(apiProcessMetaData == null) + { + apiProcessMetaData = new ApiProcessMetaData(); + withApiProcessMetaData(apiName, apiProcessMetaData); + } + return (apiProcessMetaData); + } + + + + /******************************************************************************* + ** Setter for apis + *******************************************************************************/ + public void setApis(Map apis) + { + this.apis = apis; + } + + + + /******************************************************************************* + ** Fluent setter for apis + *******************************************************************************/ + public ApiProcessMetaDataContainer withApis(Map apis) + { + this.apis = apis; + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for apis + *******************************************************************************/ + public ApiProcessMetaDataContainer withApiProcessMetaData(String apiName, ApiProcessMetaData apiProcessMetaData) + { + if(this.apis == null) + { + this.apis = new LinkedHashMap<>(); + } + this.apis.put(apiName, apiProcessMetaData); + return (this); + } +} 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..20cf7453 --- /dev/null +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessObjectOutput.java @@ -0,0 +1,243 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.api.model.metadata.processes; + + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import com.kingsrook.qqq.api.actions.GenerateOpenApiSpecAction; +import com.kingsrook.qqq.api.model.metadata.fields.ApiFieldMetaData; +import com.kingsrook.qqq.api.model.metadata.fields.ApiFieldMetaDataContainer; +import com.kingsrook.qqq.api.model.openapi.Content; +import com.kingsrook.qqq.api.model.openapi.ExampleWithListValue; +import com.kingsrook.qqq.api.model.openapi.ExampleWithSingleValue; +import com.kingsrook.qqq.api.model.openapi.Response; +import com.kingsrook.qqq.api.model.openapi.Schema; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import com.kingsrook.qqq.backend.core.utils.ObjectUtils; +import com.kingsrook.qqq.backend.core.utils.StringUtils; +import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder; +import io.javalin.http.ContentType; +import org.eclipse.jetty.http.HttpStatus; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class ApiProcessObjectOutput implements ApiProcessOutputInterface +{ + private List outputFields; + + private String responseDescription; + private HttpStatus.Code successResponseCode; + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public HttpStatus.Code getSuccessStatusCode(RunProcessInput runProcessInput, RunProcessOutput runProcessOutput) + { + return (HttpStatus.Code.OK); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public Map getSpecResponses(String apiName) + { + Map properties = new LinkedHashMap<>(); + for(QFieldMetaData outputField : CollectionUtils.nonNullList(outputFields)) + { + ApiFieldMetaDataContainer apiFieldMetaDataContainer = ApiFieldMetaDataContainer.ofOrNew(outputField); + ApiFieldMetaData apiFieldMetaData = apiFieldMetaDataContainer.getApiFieldMetaData(apiName); + + Object example = null; + if(apiFieldMetaData != null) + { + if(apiFieldMetaData.getExample() instanceof ExampleWithSingleValue exampleWithSingleValue) + { + example = exampleWithSingleValue.getValue(); + } + else if(apiFieldMetaData.getExample() instanceof ExampleWithListValue exampleWithListValue) + { + example = exampleWithListValue.getValue(); + } + } + + properties.put(outputField.getName(), new Schema() + .withDescription(apiFieldMetaData == null ? null : apiFieldMetaData.getDescription()) + .withExample(example) + .withNullable(!outputField.getIsRequired()) + .withType(GenerateOpenApiSpecAction.getFieldType(outputField)) + ); + } + + return (MapBuilder.of( + Objects.requireNonNullElse(successResponseCode, HttpStatus.Code.OK).getCode(), + new Response() + .withDescription(ObjectUtils.requireConditionElse(responseDescription, StringUtils::hasContent, "Process has been successfully executed.")) + .withContent(MapBuilder.of(ContentType.JSON, new Content() + .withSchema(new Schema() + .withType("object") + .withProperties(properties)))) + )); + } + + + + /******************************************************************************* + ** + ******************************************************************************/ + @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); + } + + + + /******************************************************************************* + ** Getter for responseDescription + *******************************************************************************/ + public String getResponseDescription() + { + return (this.responseDescription); + } + + + + /******************************************************************************* + ** Setter for responseDescription + *******************************************************************************/ + public void setResponseDescription(String responseDescription) + { + this.responseDescription = responseDescription; + } + + + + /******************************************************************************* + ** Fluent setter for responseDescription + *******************************************************************************/ + public ApiProcessObjectOutput withResponseDescription(String responseDescription) + { + this.responseDescription = responseDescription; + return (this); + } + + + + /******************************************************************************* + ** Getter for successResponseCode + *******************************************************************************/ + public HttpStatus.Code getSuccessResponseCode() + { + return (this.successResponseCode); + } + + + + /******************************************************************************* + ** Setter for successResponseCode + *******************************************************************************/ + public void setSuccessResponseCode(HttpStatus.Code successResponseCode) + { + this.successResponseCode = successResponseCode; + } + + + + /******************************************************************************* + ** Fluent setter for successResponseCode + *******************************************************************************/ + public ApiProcessObjectOutput withSuccessResponseCode(HttpStatus.Code successResponseCode) + { + this.successResponseCode = successResponseCode; + return (this); + } + +} diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessOutputInterface.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessOutputInterface.java new file mode 100644 index 00000000..6e430aa4 --- /dev/null +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessOutputInterface.java @@ -0,0 +1,64 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.api.model.metadata.processes; + + +import java.io.Serializable; +import java.util.Map; +import com.kingsrook.qqq.api.model.openapi.Response; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput; +import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder; +import org.eclipse.jetty.http.HttpStatus; + + +/******************************************************************************* + ** + *******************************************************************************/ +public interface ApiProcessOutputInterface +{ + + /******************************************************************************* + ** + *******************************************************************************/ + Serializable getOutputForProcess(RunProcessInput runProcessInput, RunProcessOutput runProcessOutput) throws QException; + + /******************************************************************************* + ** + *******************************************************************************/ + default HttpStatus.Code getSuccessStatusCode(RunProcessInput runProcessInput, RunProcessOutput runProcessOutput) + { + return (HttpStatus.Code.NO_CONTENT); + } + + /******************************************************************************* + ** + *******************************************************************************/ + default Map getSpecResponses(String apiName) + { + return (MapBuilder.of( + HttpStatus.Code.NO_CONTENT.getCode(), new Response() + .withDescription("Process has been successfully executed.") + )); + } +} diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessSummaryListOutput.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessSummaryListOutput.java new file mode 100644 index 00000000..847e4b8c --- /dev/null +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessSummaryListOutput.java @@ -0,0 +1,266 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.api.model.metadata.processes; + + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import com.kingsrook.qqq.api.model.openapi.Content; +import com.kingsrook.qqq.api.model.openapi.Response; +import com.kingsrook.qqq.api.model.openapi.Schema; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.logging.QLogger; +import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryFilterLink; +import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLine; +import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLineInterface; +import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryRecordLink; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import com.kingsrook.qqq.backend.core.utils.collections.ListBuilder; +import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder; +import io.javalin.http.ContentType; +import org.apache.commons.lang.NotImplementedException; +import org.eclipse.jetty.http.HttpStatus; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class ApiProcessSummaryListOutput implements ApiProcessOutputInterface +{ + private static final QLogger LOG = QLogger.getLogger(ApiProcessSummaryListOutput.class); + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public HttpStatus.Code getSuccessStatusCode(RunProcessInput runProcessInput, RunProcessOutput runProcessOutput) + { + List 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 Map getSpecResponses(String apiName) + { + Map propertiesFor207Object = new LinkedHashMap<>(); + propertiesFor207Object.put("id", new Schema().withType("integer").withDescription("Id of the record whose status is being described in the object")); + propertiesFor207Object.put("statusCode", new Schema().withType("integer").withDescription("HTTP Status code indicating the success or failure of the process on this record")); + propertiesFor207Object.put("statusText", new Schema().withType("string").withDescription("HTTP Status text indicating the success or failure of the process on this record")); + propertiesFor207Object.put("message", new Schema().withType("string").withDescription("Additional descriptive information about the result of the process on this record.")); + + List exampleFor207Object = ListBuilder.of(MapBuilder.of(LinkedHashMap::new) + .with("id", 42) + .with("statusCode", io.javalin.http.HttpStatus.OK.getCode()) + .with("statusText", io.javalin.http.HttpStatus.OK.getMessage()) + .with("message", "record was processed successfully.") + .build(), + MapBuilder.of(LinkedHashMap::new) + .with("id", 47) + .with("statusCode", io.javalin.http.HttpStatus.BAD_REQUEST.getCode()) + .with("statusText", io.javalin.http.HttpStatus.BAD_REQUEST.getMessage()) + .with("message", "error executing process on record.") + .build()); + + return MapBuilder.of( + HttpStatus.Code.MULTI_STATUS.getCode(), new Response() + .withDescription("For each input record, an object describing its status may be returned.") + .withContent(MapBuilder.of(ContentType.JSON, new Content() + .withSchema(new Schema() + .withType("array") + .withItems(new Schema() + .withType("object") + .withProperties(propertiesFor207Object)) + .withExample(exampleFor207Object) + ) + )), + + HttpStatus.Code.NO_CONTENT.getCode(), new Response() + .withDescription("If no records were found, there may be no content in the response.") + ); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @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.pickMessage(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) + { + apiOutput.add(toMap(processSummaryRecordLink)); + } + else if(processSummaryLineInterface instanceof ProcessSummaryFilterLink processSummaryFilterLink) + { + apiOutput.add(toMap(processSummaryFilterLink)); + } + else + { + throw new NotImplementedException("Unknown ProcessSummaryLineInterface handling"); + } + } + + return (apiOutput); + } + catch(Exception e) + { + LOG.warn("Error getting api output for process", e); + throw (new QException("Error generating process output", e)); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private HashMap toMap(ProcessSummaryFilterLink processSummaryFilterLink) + { + HashMap map = initResultMapForProcessSummaryLine(processSummaryFilterLink); + + String messagePrefix = getResultMapMessagePrefix(processSummaryFilterLink); + map.put("message", messagePrefix + processSummaryFilterLink.getFullText()); + + return (map); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private HashMap toMap(ProcessSummaryRecordLink processSummaryRecordLink) + { + HashMap map = initResultMapForProcessSummaryLine(processSummaryRecordLink); + + String messagePrefix = getResultMapMessagePrefix(processSummaryRecordLink); + map.put("message", messagePrefix + processSummaryRecordLink.getFullText()); + + return (map); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static HashMap toMap(ProcessSummaryLine processSummaryLine) + { + HashMap map = initResultMapForProcessSummaryLine(processSummaryLine); + + String messagePrefix = getResultMapMessagePrefix(processSummaryLine); + map.put("message", messagePrefix + processSummaryLine.getMessage()); + + return (map); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static String getResultMapMessagePrefix(ProcessSummaryLineInterface processSummaryLine) + { + @SuppressWarnings("checkstyle:indentation") + String messagePrefix = switch(processSummaryLine.getStatus()) + { + case OK, INFO, ERROR -> ""; + case WARNING -> "Warning: "; + }; + return messagePrefix; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static HashMap initResultMapForProcessSummaryLine(ProcessSummaryLineInterface processSummaryLine) + { + HashMap map = new HashMap<>(); + + @SuppressWarnings("checkstyle:indentation") + HttpStatus.Code code = switch(processSummaryLine.getStatus()) + { + case OK, WARNING, INFO -> HttpStatus.Code.OK; + case ERROR -> HttpStatus.Code.INTERNAL_SERVER_ERROR; + }; + + map.put("statusCode", code.getCode()); + map.put("statusText", code.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..dc75f7ad --- /dev/null +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessUtils.java @@ -0,0 +1,193 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +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.")); + } + + ApiProcessMetaDataContainer apiProcessMetaDataContainer = ApiProcessMetaDataContainer.of(process); + if(apiProcessMetaDataContainer == null) + { + LOG.info("404 because process apiProcessMetaDataContainer is null", logPairs); + throw (new QNotFoundException("Could not find a process named " + processApiName + " in this api.")); + } + + ApiProcessMetaData apiProcessMetaData = apiProcessMetaDataContainer.getApiProcessMetaData(apiInstanceMetaData.getName()); + if(apiProcessMetaData == null) + { + LOG.info("404 because process apiProcessMetaData is null", logPairs); + throw (new QNotFoundException("Could not find a process named " + processApiName + " in this api.")); + } + + if(BooleanUtils.isTrue(apiProcessMetaData.getIsExcluded())) + { + LOG.info("404 because process is excluded", logPairs); + throw (new QNotFoundException("Could not find a process named " + processApiName + " in this api.")); + } + + if(BooleanUtils.isTrue(process.getIsHidden())) + { + if(!BooleanUtils.isTrue(apiProcessMetaData.getOverrideProcessIsHidden())) + { + LOG.info("404 because process isHidden", logPairs); + throw (new QNotFoundException("Could not find a process named " + processApiName + " in this api.")); + } + } + + APIVersion requestApiVersion = new APIVersion(version); + List 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/main/java/com/kingsrook/qqq/api/model/metadata/processes/PostRunApiProcessCustomizer.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/PostRunApiProcessCustomizer.java new file mode 100644 index 00000000..e9fca81a --- /dev/null +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/PostRunApiProcessCustomizer.java @@ -0,0 +1,41 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.api.model.metadata.processes; + + +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput; + + +/******************************************************************************* + ** + *******************************************************************************/ +public interface PostRunApiProcessCustomizer +{ + + /******************************************************************************* + ** + *******************************************************************************/ + void postApiRun(RunProcessInput runProcessInput, RunProcessOutput runProcessOutput) throws QException; + +} diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/PreRunApiProcessCustomizer.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/PreRunApiProcessCustomizer.java new file mode 100644 index 00000000..a463af66 --- /dev/null +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/PreRunApiProcessCustomizer.java @@ -0,0 +1,40 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.api.model.metadata.processes; + + +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput; + + +/******************************************************************************* + ** + *******************************************************************************/ +public interface PreRunApiProcessCustomizer +{ + + /******************************************************************************* + ** + *******************************************************************************/ + void preApiRun(RunProcessInput runProcessInput) throws QException; + +} diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/tables/ApiTableMetaData.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/tables/ApiTableMetaData.java index 5b3a1aa0..ec21098b 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/tables/ApiTableMetaData.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/tables/ApiTableMetaData.java @@ -27,7 +27,7 @@ import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; -import com.kingsrook.qqq.api.ApiMiddlewareType; +import com.kingsrook.qqq.api.ApiSupplementType; import com.kingsrook.qqq.api.model.APIVersionRange; import com.kingsrook.qqq.api.model.metadata.ApiOperation; import com.kingsrook.qqq.api.model.metadata.fields.ApiFieldMetaData; @@ -81,7 +81,7 @@ public class ApiTableMetaData implements ApiOperation.EnabledOperationsProvider { for(QFieldMetaData field : table.getFields().values()) { - ApiFieldMetaData apiFieldMetaData = ensureFieldHasApiMiddlewareMetaData(apiName, field); + ApiFieldMetaData apiFieldMetaData = ensureFieldHasApiSupplementalMetaData(apiName, field); if(apiFieldMetaData.getInitialVersion() == null) { apiFieldMetaData.setInitialVersion(initialVersion); @@ -90,7 +90,7 @@ public class ApiTableMetaData implements ApiOperation.EnabledOperationsProvider for(QFieldMetaData field : CollectionUtils.nonNullList(removedApiFields)) { - ApiFieldMetaData apiFieldMetaData = ensureFieldHasApiMiddlewareMetaData(apiName, field); + ApiFieldMetaData apiFieldMetaData = ensureFieldHasApiSupplementalMetaData(apiName, field); if(apiFieldMetaData.getInitialVersion() == null) { apiFieldMetaData.setInitialVersion(initialVersion); @@ -104,11 +104,11 @@ public class ApiTableMetaData implements ApiOperation.EnabledOperationsProvider /******************************************************************************* ** *******************************************************************************/ - private static ApiFieldMetaData ensureFieldHasApiMiddlewareMetaData(String apiName, QFieldMetaData field) + private static ApiFieldMetaData ensureFieldHasApiSupplementalMetaData(String apiName, QFieldMetaData field) { - if(field.getMiddlewareMetaData(ApiMiddlewareType.NAME) == null) + if(field.getSupplementalMetaData(ApiSupplementType.NAME) == null) { - field.withMiddlewareMetaData(new ApiFieldMetaDataContainer()); + field.withSupplementalMetaData(new ApiFieldMetaDataContainer()); } ApiFieldMetaDataContainer apiFieldMetaDataContainer = ApiFieldMetaDataContainer.of(field); diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/tables/ApiTableMetaDataContainer.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/tables/ApiTableMetaDataContainer.java index 0b38caed..8dd779fc 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/tables/ApiTableMetaDataContainer.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/tables/ApiTableMetaDataContainer.java @@ -24,8 +24,8 @@ package com.kingsrook.qqq.api.model.metadata.tables; import java.util.LinkedHashMap; import java.util.Map; -import com.kingsrook.qqq.api.ApiMiddlewareType; -import com.kingsrook.qqq.backend.core.model.metadata.tables.QMiddlewareTableMetaData; +import com.kingsrook.qqq.api.ApiSupplementType; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QSupplementalTableMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; @@ -33,7 +33,7 @@ import com.kingsrook.qqq.backend.core.utils.CollectionUtils; /******************************************************************************* ** *******************************************************************************/ -public class ApiTableMetaDataContainer extends QMiddlewareTableMetaData +public class ApiTableMetaDataContainer extends QSupplementalTableMetaData { private Map apis; @@ -55,7 +55,7 @@ public class ApiTableMetaDataContainer extends QMiddlewareTableMetaData *******************************************************************************/ public static ApiTableMetaDataContainer of(QTableMetaData table) { - return ((ApiTableMetaDataContainer) table.getMiddlewareMetaData(ApiMiddlewareType.NAME)); + return ((ApiTableMetaDataContainer) table.getSupplementalMetaData(ApiSupplementType.NAME)); } diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/ExampleWithSingleValue.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/ExampleWithSingleValue.java index e365e16b..79ebab6c 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/ExampleWithSingleValue.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/ExampleWithSingleValue.java @@ -22,12 +22,15 @@ package com.kingsrook.qqq.api.model.openapi; +import java.io.Serializable; + + /******************************************************************************* ** *******************************************************************************/ public class ExampleWithSingleValue extends Example { - private String value; + private Serializable value; @@ -46,7 +49,7 @@ public class ExampleWithSingleValue extends Example /******************************************************************************* ** Getter for value *******************************************************************************/ - public String getValue() + public Serializable getValue() { return (this.value); } @@ -56,7 +59,7 @@ public class ExampleWithSingleValue extends Example /******************************************************************************* ** Setter for value *******************************************************************************/ - public void setValue(String value) + public void setValue(Serializable value) { this.value = value; } @@ -66,7 +69,7 @@ public class ExampleWithSingleValue extends Example /******************************************************************************* ** Fluent setter for value *******************************************************************************/ - public ExampleWithSingleValue withValue(String value) + public ExampleWithSingleValue withValue(Serializable value) { this.value = value; return (this); diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/HttpMethod.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/HttpMethod.java new file mode 100644 index 00000000..6d6a8dad --- /dev/null +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/HttpMethod.java @@ -0,0 +1,35 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.api.model.openapi; + + +/******************************************************************************* + ** + *******************************************************************************/ +public enum HttpMethod +{ + GET, + POST, + PUT, + PATCH, + DELETE +} diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/Parameter.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/Parameter.java index 4077f25b..9d43451f 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/Parameter.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/Parameter.java @@ -37,6 +37,7 @@ public class Parameter private Schema schema; private Boolean explode; private Map examples; + private Example example; @@ -255,4 +256,35 @@ public class Parameter return (this); } + + + /******************************************************************************* + ** Getter for example + *******************************************************************************/ + public Example getExample() + { + return (this.example); + } + + + + /******************************************************************************* + ** Setter for examplee + *******************************************************************************/ + public void setExample(Example example) + { + this.example = example; + } + + + + /******************************************************************************* + ** Fluent setter for examplee + *******************************************************************************/ + public Parameter withExample(Example example) + { + this.example = example; + return (this); + } + } diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/Schema.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/Schema.java index 228bae49..3f858881 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/Schema.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/Schema.java @@ -22,6 +22,7 @@ package com.kingsrook.qqq.api.model.openapi; +import java.math.BigDecimal; import java.util.List; import java.util.Map; import com.fasterxml.jackson.annotation.JsonGetter; @@ -191,6 +192,27 @@ public class Schema + /******************************************************************************* + ** Setter for example + *******************************************************************************/ + public void setExample(BigDecimal example) + { + this.example = example; + } + + + + /******************************************************************************* + ** Fluent setter for example + *******************************************************************************/ + public Schema withExample(Object example) + { + this.example = example; + return (this); + } + + + /******************************************************************************* ** Fluent setter for example *******************************************************************************/ diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/utils/ApiScriptUtils.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/utils/ApiScriptUtils.java index f06585d2..5c700b80 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/utils/ApiScriptUtils.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/utils/ApiScriptUtils.java @@ -30,21 +30,30 @@ import java.util.Map; import com.kingsrook.qqq.api.actions.ApiImplementation; import com.kingsrook.qqq.api.actions.QRecordApiAdapter; import com.kingsrook.qqq.api.model.APIVersion; +import com.kingsrook.qqq.api.model.actions.HttpApiResponse; import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaData; import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaDataContainer; +import com.kingsrook.qqq.backend.core.actions.scripts.QCodeExecutor; +import com.kingsrook.qqq.backend.core.actions.scripts.QCodeExecutorAware; import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.utils.JsonUtils; +import com.kingsrook.qqq.backend.core.utils.StringUtils; +import com.kingsrook.qqq.backend.core.utils.ValueUtils; +import org.json.JSONObject; /******************************************************************************* ** Object injected into script context, for interfacing with a QQQ API. *******************************************************************************/ -public class ApiScriptUtils implements Serializable +public class ApiScriptUtils implements QCodeExecutorAware, Serializable { private String apiName; private String apiVersion; + private QCodeExecutor qCodeExecutor; + /******************************************************************************* @@ -165,6 +174,7 @@ public class ApiScriptUtils implements Serializable public Map insert(String tableApiName, Object body) throws QException { validateApiNameAndVersion("insert(" + tableApiName + ")"); + body = processBodyToJsonString(body); return (ApiImplementation.insert(getApiInstanceMetaData(), apiVersion, tableApiName, String.valueOf(body))); } @@ -176,6 +186,7 @@ public class ApiScriptUtils implements Serializable public List> bulkInsert(String tableApiName, Object body) throws QException { validateApiNameAndVersion("bulkInsert(" + tableApiName + ")"); + body = processBodyToJsonString(body); return (ApiImplementation.bulkInsert(getApiInstanceMetaData(), apiVersion, tableApiName, String.valueOf(body))); } @@ -187,17 +198,61 @@ public class ApiScriptUtils implements Serializable public void update(String tableApiName, Object primaryKey, Object body) throws QException { validateApiNameAndVersion("update(" + tableApiName + "," + primaryKey + ")"); + body = processBodyToJsonString(body); ApiImplementation.update(getApiInstanceMetaData(), apiVersion, tableApiName, String.valueOf(primaryKey), String.valueOf(body)); } + /******************************************************************************* + ** Take a "body" object, which maybe defined in the script's language/run-time, + ** and try to process it into a JSON String (which is what the API Implementation wants) + *******************************************************************************/ + private Object processBodyToJsonString(Object body) + { + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // if the caller already supplied the object as a string, then return that string. // + // and in case it can't be parsed as json, well, let that error come out of the api implementation, and go back to the caller. // + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + if(body instanceof String) + { + return (body); + } + + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // if the input body wasn't a json string, try to convert it from a language-type object (e.g., javscript) to a java-object, // + // then make JSON out of that for the APIImplementation // + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + Object bodyJavaObject = processInputObjectViaCodeExecutor(body); + return JsonUtils.toJson(bodyJavaObject); + } + + + + /******************************************************************************* + ** Use the QCodeExecutor (if we have one) to process an input object from the + ** script's language into a (more) native java object. + ** e.g., a Nashorn ScriptObjectMirror will end up as a "primitive", or a List or Map of such + *******************************************************************************/ + private Object processInputObjectViaCodeExecutor(Object body) + { + if(qCodeExecutor == null || body == null) + { + return (body); + } + + return (qCodeExecutor.convertObjectToJava(body)); + } + + + /******************************************************************************* ** *******************************************************************************/ public List> bulkUpdate(String tableApiName, Object body) throws QException { validateApiNameAndVersion("bulkUpdate(" + tableApiName + ")"); + body = processBodyToJsonString(body); return (ApiImplementation.bulkUpdate(getApiInstanceMetaData(), apiVersion, tableApiName, String.valueOf(body))); } @@ -220,11 +275,64 @@ public class ApiScriptUtils implements Serializable public List> bulkDelete(String tableApiName, Object body) throws QException { validateApiNameAndVersion("bulkDelete(" + tableApiName + ")"); + body = processBodyToJsonString(body); return (ApiImplementation.bulkDelete(getApiInstanceMetaData(), apiVersion, tableApiName, String.valueOf(body))); } + /******************************************************************************* + ** + *******************************************************************************/ + public Serializable runProcess(String processApiName) throws QException + { + return (runProcess(processApiName, null)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public Serializable runProcess(String processApiName, Object params) throws QException + { + validateApiNameAndVersion("runProcess(" + processApiName + ")"); + + Map paramMap = new LinkedHashMap<>(); + + if(params != null) + { + params = processBodyToJsonString(params); + String paramsString = ValueUtils.getValueAsString(params); + if(StringUtils.hasContent(paramsString)) + { + JSONObject paramsJSON = new JSONObject(paramsString); + for(String fieldName : paramsJSON.keySet()) + { + paramMap.put(fieldName, paramsJSON.optString(fieldName)); + } + } + } + + HttpApiResponse httpApiResponse = ApiImplementation.runProcess(getApiInstanceMetaData(), apiVersion, processApiName, paramMap); + return (httpApiResponse.getResponseBodyObject()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public Serializable getProcessStatus(String processApiName, String jobId) throws QException + { + validateApiNameAndVersion("getProcessStatus(" + processApiName + ")"); + + HttpApiResponse httpApiResponse = ApiImplementation.getProcessStatus(getApiInstanceMetaData(), apiVersion, processApiName, jobId); + return (httpApiResponse.getResponseBodyObject()); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -257,4 +365,15 @@ public class ApiScriptUtils implements Serializable } return paramMap; } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void setQCodeExecutor(QCodeExecutor qCodeExecutor) + { + this.qCodeExecutor = qCodeExecutor; + } } diff --git a/qqq-middleware-api/src/main/resources/rapidoc/rapidoc-container.html b/qqq-middleware-api/src/main/resources/rapidoc/rapidoc-container.html index 8be1775d..540a5a24 100644 --- a/qqq-middleware-api/src/main/resources/rapidoc/rapidoc-container.html +++ b/qqq-middleware-api/src/main/resources/rapidoc/rapidoc-container.html @@ -38,7 +38,7 @@ show-header="false" allow-spec-file-download="true" primary-color="{primaryColor}" - sort-endpoints-by="method" + sort-endpoints-by="none" allow-authentication="true" persist-auth="true" render-style="focused" diff --git a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/GetPersonInfoStep.java b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/GetPersonInfoStep.java new file mode 100644 index 00000000..6ee02038 --- /dev/null +++ b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/GetPersonInfoStep.java @@ -0,0 +1,48 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.api; + + +import java.math.BigDecimal; +import com.kingsrook.qqq.backend.core.actions.processes.BackendStep; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class GetPersonInfoStep implements BackendStep +{ + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException + { + runBackendStepOutput.addValue("density", new BigDecimal("3.50")); + runBackendStepOutput.addValue("daysOld", runBackendStepInput.getValueInteger("age") * 365); + runBackendStepOutput.addValue("nickname", "Guy from " + runBackendStepInput.getValueString("homeTown")); + } + +} 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 428a56a6..fea81122 100644 --- a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/TestUtils.java +++ b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/TestUtils.java @@ -29,8 +29,15 @@ import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaData; import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaDataContainer; import com.kingsrook.qqq.api.model.metadata.fields.ApiFieldMetaData; import com.kingsrook.qqq.api.model.metadata.fields.ApiFieldMetaDataContainer; +import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessInput; +import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessInputFieldsContainer; +import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessMetaData; +import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessMetaDataContainer; +import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessObjectOutput; +import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessSummaryListOutput; import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaData; import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaDataContainer; +import com.kingsrook.qqq.api.model.openapi.HttpMethod; import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPreDeleteCustomizer; import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPreInsertCustomizer; import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPreUpdateCustomizer; @@ -45,12 +52,23 @@ import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.authentication.Auth0AuthenticationMetaData; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; +import com.kingsrook.qqq.backend.core.model.metadata.dashboard.nocode.HtmlWrapper; +import com.kingsrook.qqq.backend.core.model.metadata.dashboard.nocode.WidgetHtmlLine; import com.kingsrook.qqq.backend.core.model.metadata.fields.DisplayFormat; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn; import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinType; import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.PVSValueFormatAndFields; +import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource; +import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType; +import com.kingsrook.qqq.backend.core.model.metadata.processes.NoCodeWidgetFrontendComponentMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QComponentType; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendComponentMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.Association; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey; @@ -58,6 +76,9 @@ import com.kingsrook.qqq.backend.core.model.statusmessages.BadInputStatusMessage import com.kingsrook.qqq.backend.core.model.statusmessages.QWarningMessage; import com.kingsrook.qqq.backend.core.model.statusmessages.SystemErrorStatusMessage; import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryBackendModule; +import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.ExtractViaQueryStep; +import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.LoadViaUpdateStep; +import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.StreamedETLWithFrontendProcess; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; @@ -74,6 +95,9 @@ public class TestUtils public static final String TABLE_NAME_LINE_ITEM_EXTRINSIC = "orderLineExtrinsic"; public static final String TABLE_NAME_ORDER_EXTRINSIC = "orderExtrinsic"; + public static final String PROCESS_NAME_GET_PERSON_INFO = "getPersonInfo"; + public static final String PROCESS_NAME_TRANSFORM_PEOPLE = "transformPeople"; + public static final String API_NAME = "test-api"; public static final String ALTERNATIVE_API_NAME = "person-api"; @@ -103,9 +127,13 @@ public class TestUtils qInstance.addJoin(defineJoinLineItemLineItemExtrinsic()); qInstance.addJoin(defineJoinOrderOrderExtrinsic()); + qInstance.addPossibleValueSource(definePersonPossibleValueSource()); + qInstance.addProcess(defineProcessGetPersonInfo()); + qInstance.addProcess(defineProcessTransformPeople()); + qInstance.setAuthentication(new Auth0AuthenticationMetaData().withType(QAuthenticationType.FULLY_ANONYMOUS).withName("anonymous")); - qInstance.withMiddlewareMetaData(new ApiInstanceMetaDataContainer() + qInstance.withSupplementalMetaData(new ApiInstanceMetaDataContainer() .withApiInstanceMetaData(new ApiInstanceMetaData() .withName(API_NAME) .withPath("/api/") @@ -133,6 +161,109 @@ public class TestUtils + /******************************************************************************* + ** + *******************************************************************************/ + private static QPossibleValueSource definePersonPossibleValueSource() + { + return new QPossibleValueSource() + .withName(TABLE_NAME_PERSON) + .withType(QPossibleValueSourceType.TABLE) + .withTableName(TABLE_NAME_PERSON) + .withValueFormatAndFields(PVSValueFormatAndFields.LABEL_ONLY); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static QProcessMetaData defineProcessGetPersonInfo() + { + QProcessMetaData process = new QProcessMetaData() + .withName(PROCESS_NAME_GET_PERSON_INFO) + .withLabel("Get Person Info") + .withTableName(TABLE_NAME_PERSON) + .addStep(new QFrontendStepMetaData() + .withName("enterInputs") + .withLabel("Person Info Input") + .withComponent(new QFrontendComponentMetaData().withType(QComponentType.EDIT_FORM)) + + .withFormField(new QFieldMetaData("age", QFieldType.INTEGER).withIsRequired(true)) + .withFormField(new QFieldMetaData("partnerPersonId", QFieldType.INTEGER).withPossibleValueSourceName(TABLE_NAME_PERSON)) + .withFormField(new QFieldMetaData("heightInches", QFieldType.DECIMAL).withIsRequired(true)) + .withFormField(new QFieldMetaData("weightPounds", QFieldType.INTEGER).withIsRequired(true)) + .withFormField(new QFieldMetaData("homeTown", QFieldType.STRING).withIsRequired(true)) + + .withComponent(new NoCodeWidgetFrontendComponentMetaData() + + .withOutput(new WidgetHtmlLine() + .withWrapper(HtmlWrapper.divWithStyles(HtmlWrapper.STYLE_FLOAT_RIGHT, HtmlWrapper.STYLE_MEDIUM_CENTERED, HtmlWrapper.styleWidth("50%"))) + .withVelocityTemplate(""" + Density:
$density
+ """)) + + .withOutput(new WidgetHtmlLine() + .withVelocityTemplate(""" + Days old: $daysOld
+ Nickname: $nickname
+ """)) + )) + + .addStep(new QBackendStepMetaData() + .withName("execute") + .withCode(new QCodeReference(GetPersonInfoStep.class))) + + .addStep(new QFrontendStepMetaData() + .withName("dummyStep") + ); + + process.withSupplementalMetaData(new ApiProcessMetaDataContainer() + .withApiProcessMetaData(API_NAME, new ApiProcessMetaData() + .withInitialVersion(CURRENT_API_VERSION) + .withMethod(HttpMethod.GET) + .withInput(new ApiProcessInput() + .withQueryStringParams(new ApiProcessInputFieldsContainer().withInferredInputFields(process))) + .withOutput(new ApiProcessObjectOutput() + .withOutputField(new QFieldMetaData("density", QFieldType.DECIMAL)) + .withOutputField(new QFieldMetaData("daysOld", QFieldType.INTEGER)) + .withOutputField(new QFieldMetaData("nickname", QFieldType.STRING))) + )); + + return (process); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static QProcessMetaData defineProcessTransformPeople() + { + QProcessMetaData process = StreamedETLWithFrontendProcess.processMetaDataBuilder() + .withName(PROCESS_NAME_TRANSFORM_PEOPLE) + .withTableName(TABLE_NAME_PERSON) + .withSourceTable(TABLE_NAME_PERSON) + .withDestinationTable(TABLE_NAME_PERSON) + .withMinInputRecords(1) + .withExtractStepClass(ExtractViaQueryStep.class) + .withTransformStepClass(TransformPersonStep.class) + .withLoadStepClass(LoadViaUpdateStep.class) + .getProcessMetaData(); + + process.withSupplementalMetaData(new ApiProcessMetaDataContainer() + .withApiProcessMetaData(API_NAME, new ApiProcessMetaData() + .withInitialVersion(CURRENT_API_VERSION) + .withMethod(HttpMethod.POST) + .withInput(new ApiProcessInput() + .withQueryStringParams(new ApiProcessInputFieldsContainer().withRecordIdsField(new QFieldMetaData("id", QFieldType.STRING)))) + .withOutput(new ApiProcessSummaryListOutput()))); + + return (process); + } + + + /******************************************************************************* ** Define the in-memory backend used in standard tests *******************************************************************************/ @@ -204,7 +335,7 @@ public class TestUtils /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // make some changes to this table in the "main" api (but leave it like the backend in the ALTERNATIVE_API_NAME) // /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - table.withMiddlewareMetaData(new ApiTableMetaDataContainer() + table.withSupplementalMetaData(new ApiTableMetaDataContainer() .withApiTableMetaData(API_NAME, new ApiTableMetaData() .withInitialVersion(V2022_Q4) @@ -212,7 +343,7 @@ public class TestUtils // in 2022.Q4, this table had a "shoeCount" field. but for the 2023.Q1 version, we renamed it to noOfShoes! // ////////////////////////////////////////////////////////////////////////////////////////////////////////////// .withRemovedApiField(new QFieldMetaData("shoeCount", QFieldType.INTEGER).withDisplayFormat(DisplayFormat.COMMAS) - .withMiddlewareMetaData(new ApiFieldMetaDataContainer().withApiFieldMetaData(API_NAME, + .withSupplementalMetaData(new ApiFieldMetaDataContainer().withApiFieldMetaData(API_NAME, new ApiFieldMetaData().withFinalVersion(V2022_Q4).withReplacedByFieldName("noOfShoes")))) ) .withApiTableMetaData(ALTERNATIVE_API_NAME, new ApiTableMetaData().withInitialVersion(V2022_Q4))); @@ -220,18 +351,18 @@ public class TestUtils ///////////////////////////////////////////////////// // change the name for this field for the main api // ///////////////////////////////////////////////////// - table.getField("birthDate").withMiddlewareMetaData(new ApiFieldMetaDataContainer().withApiFieldMetaData(API_NAME, new ApiFieldMetaData().withApiFieldName("birthDay"))); + table.getField("birthDate").withSupplementalMetaData(new ApiFieldMetaDataContainer().withApiFieldMetaData(API_NAME, new ApiFieldMetaData().withApiFieldName("birthDay"))); //////////////////////////////////////////////////////////////////////////////// // See above - we renamed this field (in the backend) for the 2023_Q1 version // //////////////////////////////////////////////////////////////////////////////// - table.getField("noOfShoes").withMiddlewareMetaData(new ApiFieldMetaDataContainer().withApiFieldMetaData(API_NAME, new ApiFieldMetaData().withInitialVersion(V2023_Q1))); + table.getField("noOfShoes").withSupplementalMetaData(new ApiFieldMetaDataContainer().withApiFieldMetaData(API_NAME, new ApiFieldMetaData().withInitialVersion(V2023_Q1))); ///////////////////////////////////////////////////////////////////////////////////////////////// // 2 new fields - one will appear in a future version of the API, the other is always excluded // ///////////////////////////////////////////////////////////////////////////////////////////////// - table.getField("cost").withMiddlewareMetaData(new ApiFieldMetaDataContainer().withApiFieldMetaData(API_NAME, new ApiFieldMetaData().withInitialVersion(V2023_Q2))); - table.getField("price").withMiddlewareMetaData(new ApiFieldMetaDataContainer().withApiFieldMetaData(API_NAME, new ApiFieldMetaData().withIsExcluded(true))); + table.getField("cost").withSupplementalMetaData(new ApiFieldMetaDataContainer().withApiFieldMetaData(API_NAME, new ApiFieldMetaData().withInitialVersion(V2023_Q2))); + table.getField("price").withSupplementalMetaData(new ApiFieldMetaDataContainer().withApiFieldMetaData(API_NAME, new ApiFieldMetaData().withIsExcluded(true))); return (table); } @@ -248,7 +379,7 @@ public class TestUtils .withCustomizer(TableCustomizers.PRE_INSERT_RECORD.getRole(), new QCodeReference(OrderPreInsertCustomizer.class)) .withCustomizer(TableCustomizers.PRE_UPDATE_RECORD.getRole(), new QCodeReference(OrderPreUpdateCustomizer.class)) .withBackendName(MEMORY_BACKEND_NAME) - .withMiddlewareMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData().withInitialVersion(V2022_Q4))) + .withSupplementalMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData().withInitialVersion(V2022_Q4))) .withPrimaryKeyField("id") .withAssociation(new Association().withName("orderLines").withAssociatedTableName(TABLE_NAME_LINE_ITEM).withJoinName("orderLineItem")) .withAssociation(new Association().withName("extrinsics").withAssociatedTableName(TABLE_NAME_ORDER_EXTRINSIC).withJoinName("orderOrderExtrinsic")) @@ -271,7 +402,7 @@ public class TestUtils return new QTableMetaData() .withName(TABLE_NAME_LINE_ITEM) .withBackendName(MEMORY_BACKEND_NAME) - .withMiddlewareMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData().withInitialVersion(V2022_Q4))) + .withSupplementalMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData().withInitialVersion(V2022_Q4))) .withPrimaryKeyField("id") .withAssociation(new Association().withName("extrinsics").withAssociatedTableName(TABLE_NAME_LINE_ITEM_EXTRINSIC).withJoinName("lineItemLineItemExtrinsic")) .withField(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false)) @@ -293,7 +424,7 @@ public class TestUtils return new QTableMetaData() .withName(TABLE_NAME_LINE_ITEM_EXTRINSIC) .withBackendName(MEMORY_BACKEND_NAME) - .withMiddlewareMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData().withInitialVersion(V2022_Q4))) + .withSupplementalMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData().withInitialVersion(V2022_Q4))) .withPrimaryKeyField("id") .withField(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false)) .withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME).withIsEditable(false)) @@ -313,7 +444,7 @@ public class TestUtils return new QTableMetaData() .withName(TABLE_NAME_ORDER_EXTRINSIC) .withBackendName(MEMORY_BACKEND_NAME) - .withMiddlewareMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData().withInitialVersion(V2022_Q4))) + .withSupplementalMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData().withInitialVersion(V2022_Q4))) .withPrimaryKeyField("id") .withField(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false)) .withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME).withIsEditable(false)) 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..d3dfdd0a --- /dev/null +++ b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/TransformPersonStep.java @@ -0,0 +1,80 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +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/actions/GenerateOpenApiSpecActionTest.java b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/actions/GenerateOpenApiSpecActionTest.java index acd4e4f1..902cebf8 100644 --- a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/actions/GenerateOpenApiSpecActionTest.java +++ b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/actions/GenerateOpenApiSpecActionTest.java @@ -34,6 +34,7 @@ import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaData; import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaDataContainer; import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.instances.QInstanceEnricher; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; @@ -120,7 +121,7 @@ class GenerateOpenApiSpecActionTest extends BaseTest .withBackendName(TestUtils.MEMORY_BACKEND_NAME) .withPrimaryKeyField("id") .withField(new QFieldMetaData("id", QFieldType.INTEGER)) - .withMiddlewareMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData() + .withSupplementalMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData() .withInitialVersion(TestUtils.V2022_Q4)))); qInstance.addTable(new QTableMetaData() @@ -129,7 +130,7 @@ class GenerateOpenApiSpecActionTest extends BaseTest .withPrimaryKeyField("id") .withField(new QFieldMetaData("id", QFieldType.INTEGER)) .withIsHidden(true) - .withMiddlewareMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData() + .withSupplementalMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData() .withInitialVersion(TestUtils.V2022_Q4)))); qInstance.addTable(new QTableMetaData() @@ -137,7 +138,7 @@ class GenerateOpenApiSpecActionTest extends BaseTest .withBackendName(TestUtils.MEMORY_BACKEND_NAME) .withPrimaryKeyField("id") .withField(new QFieldMetaData("id", QFieldType.INTEGER)) - .withMiddlewareMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData() + .withSupplementalMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData() .withIsExcluded(true)))); qInstance.addTable(new QTableMetaData() @@ -151,7 +152,7 @@ class GenerateOpenApiSpecActionTest extends BaseTest .withBackendName(TestUtils.MEMORY_BACKEND_NAME) .withPrimaryKeyField("id") .withField(new QFieldMetaData("id", QFieldType.INTEGER)) - .withMiddlewareMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData() + .withSupplementalMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData() .withInitialVersion(TestUtils.V2023_Q2)))); qInstance.addTable(new QTableMetaData() @@ -159,7 +160,7 @@ class GenerateOpenApiSpecActionTest extends BaseTest .withBackendName(TestUtils.MEMORY_BACKEND_NAME) .withPrimaryKeyField("id") .withField(new QFieldMetaData("id", QFieldType.INTEGER)) - .withMiddlewareMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData() + .withSupplementalMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData() .withInitialVersion(TestUtils.V2022_Q4) .withFinalVersion(TestUtils.V2022_Q4)))); @@ -169,9 +170,11 @@ class GenerateOpenApiSpecActionTest extends BaseTest .withPrimaryKeyField("id") .withField(new QFieldMetaData("id", QFieldType.INTEGER)) .withoutCapabilities(Capability.TABLE_QUERY, Capability.TABLE_GET, Capability.TABLE_INSERT, Capability.TABLE_UPDATE, Capability.TABLE_DELETE) - .withMiddlewareMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData() + .withSupplementalMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData() .withInitialVersion(TestUtils.V2022_Q4)))); + new QInstanceEnricher(qInstance).enrich(); + GenerateOpenApiSpecOutput output = new GenerateOpenApiSpecAction().execute(new GenerateOpenApiSpecInput().withVersion(TestUtils.CURRENT_API_VERSION).withApiName(TestUtils.API_NAME)); Set apiPaths = output.getOpenAPI().getPaths().keySet(); assertTrue(apiPaths.stream().anyMatch(s -> s.contains("/supportedTable/"))); @@ -198,9 +201,11 @@ class GenerateOpenApiSpecActionTest extends BaseTest .withBackendName(TestUtils.MEMORY_BACKEND_NAME) .withPrimaryKeyField("id") .withField(new QFieldMetaData("id", QFieldType.INTEGER)) - .withMiddlewareMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData() + .withSupplementalMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData() .withApiTableName("externalName") .withInitialVersion(TestUtils.V2022_Q4)))); + + new QInstanceEnricher(qInstance).enrich(); GenerateOpenApiSpecOutput output = new GenerateOpenApiSpecAction().execute(new GenerateOpenApiSpecInput().withVersion(TestUtils.CURRENT_API_VERSION).withApiName(TestUtils.API_NAME)); Set apiPaths = output.getOpenAPI().getPaths().keySet(); diff --git a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/actions/GetTableApiFieldsActionTest.java b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/actions/GetTableApiFieldsActionTest.java index 0285b790..ee9bfc8f 100644 --- a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/actions/GetTableApiFieldsActionTest.java +++ b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/actions/GetTableApiFieldsActionTest.java @@ -74,11 +74,11 @@ class GetTableApiFieldsActionTest extends BaseTest QInstance qInstance = QContext.getQInstance(); qInstance.addTable(new QTableMetaData() .withName(TABLE_NAME) - .withMiddlewareMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData().withInitialVersion("1"))) + .withSupplementalMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData().withInitialVersion("1"))) .withField(new QFieldMetaData("a", STRING)) // inherit versionRange from the table - .withField(new QFieldMetaData("b", STRING).withMiddlewareMetaData(new ApiFieldMetaDataContainer().withApiFieldMetaData(TestUtils.API_NAME, new ApiFieldMetaData().withInitialVersion("1")))) - .withField(new QFieldMetaData("c", STRING).withMiddlewareMetaData(new ApiFieldMetaDataContainer().withApiFieldMetaData(TestUtils.API_NAME, new ApiFieldMetaData().withInitialVersion("2")))) - .withField(new QFieldMetaData("d", STRING).withMiddlewareMetaData(new ApiFieldMetaDataContainer().withApiFieldMetaData(TestUtils.API_NAME, new ApiFieldMetaData().withInitialVersion("3")))) + .withField(new QFieldMetaData("b", STRING).withSupplementalMetaData(new ApiFieldMetaDataContainer().withApiFieldMetaData(TestUtils.API_NAME, new ApiFieldMetaData().withInitialVersion("1")))) + .withField(new QFieldMetaData("c", STRING).withSupplementalMetaData(new ApiFieldMetaDataContainer().withApiFieldMetaData(TestUtils.API_NAME, new ApiFieldMetaData().withInitialVersion("2")))) + .withField(new QFieldMetaData("d", STRING).withSupplementalMetaData(new ApiFieldMetaDataContainer().withApiFieldMetaData(TestUtils.API_NAME, new ApiFieldMetaData().withInitialVersion("3")))) ); new QInstanceEnricher(qInstance).enrich(); @@ -98,13 +98,13 @@ class GetTableApiFieldsActionTest extends BaseTest QInstance qInstance = QContext.getQInstance(); qInstance.addTable(new QTableMetaData() .withName(TABLE_NAME) - .withMiddlewareMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData().withInitialVersion("1") - .withRemovedApiField(new QFieldMetaData("c", STRING).withMiddlewareMetaData(new ApiFieldMetaDataContainer().withApiFieldMetaData(TestUtils.API_NAME, new ApiFieldMetaData().withInitialVersion("1").withFinalVersion("2")))) + .withSupplementalMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData().withInitialVersion("1") + .withRemovedApiField(new QFieldMetaData("c", STRING).withSupplementalMetaData(new ApiFieldMetaDataContainer().withApiFieldMetaData(TestUtils.API_NAME, new ApiFieldMetaData().withInitialVersion("1").withFinalVersion("2")))) )) .withField(new QFieldMetaData("a", STRING)) // inherit versionRange from the table - .withField(new QFieldMetaData("b", STRING).withMiddlewareMetaData(new ApiFieldMetaDataContainer().withApiFieldMetaData(TestUtils.API_NAME, new ApiFieldMetaData().withInitialVersion("1")))) + .withField(new QFieldMetaData("b", STRING).withSupplementalMetaData(new ApiFieldMetaDataContainer().withApiFieldMetaData(TestUtils.API_NAME, new ApiFieldMetaData().withInitialVersion("1")))) // we used to have "c" here... now it's in the removed list above! - .withField(new QFieldMetaData("d", STRING).withMiddlewareMetaData(new ApiFieldMetaDataContainer().withApiFieldMetaData(TestUtils.API_NAME, new ApiFieldMetaData().withInitialVersion("3")))) + .withField(new QFieldMetaData("d", STRING).withSupplementalMetaData(new ApiFieldMetaDataContainer().withApiFieldMetaData(TestUtils.API_NAME, new ApiFieldMetaData().withInitialVersion("3")))) ); new QInstanceEnricher(qInstance).enrich(); 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 0d288270..99ba063e 100644 --- a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandlerTest.java +++ b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandlerTest.java @@ -26,6 +26,7 @@ import java.time.LocalDate; import java.time.Month; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.TimeUnit; import com.kingsrook.qqq.api.BaseTest; import com.kingsrook.qqq.api.TestUtils; import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaData; @@ -50,6 +51,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.modules.authentication.implementations.FullyAnonymousAuthenticationModule; +import com.kingsrook.qqq.backend.core.utils.SleepUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.javalin.QJavalinImplementation; import io.javalin.apibuilder.EndpointGroup; @@ -67,8 +69,10 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; /******************************************************************************* @@ -98,7 +102,7 @@ class QJavalinApiHandlerTest extends BaseTest .withBackendName(TestUtils.MEMORY_BACKEND_NAME) .withPrimaryKeyField("id") .withField(new QFieldMetaData("id", QFieldType.INTEGER)) - .withMiddlewareMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData() + .withSupplementalMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData() .withApiTableName("externalName") .withInitialVersion(TestUtils.V2022_Q4)))); @@ -1439,6 +1443,82 @@ class QJavalinApiHandlerTest extends BaseTest + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testGetProcessForObject() throws QException + { + HttpResponse response = Unirest.post(BASE_URL + "/api/" + VERSION + "/person/getPersonInfo").asString(); + assertErrorResponse(HttpStatus.METHOD_NOT_ALLOWED_405, "This path only supports method: GET", response); + + response = Unirest.get(BASE_URL + "/api/" + VERSION + "/person/getPersonInfo").asString(); + assertErrorResponse(HttpStatus.BAD_REQUEST_400, "Request failed with 4 reasons: Missing value for required input field", response); + + response = Unirest.get(BASE_URL + "/api/" + VERSION + "/person/getPersonInfo?age=43&partnerPersonId=1&heightInches=72&weightPounds=220&homeTown=Chester").asString(); + assertEquals(HttpStatus.OK_200, response.getStatus()); + JSONObject jsonObject = new JSONObject(response.getBody()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testPostProcessForProcessSummaryList() throws QException + { + insertSimpsons(); + + HttpResponse response = Unirest.get(BASE_URL + "/api/" + VERSION + "/person/transformPeople").asString(); + assertErrorResponse(HttpStatus.METHOD_NOT_ALLOWED_405, "This path only supports method: POST", response); + + response = Unirest.post(BASE_URL + "/api/" + VERSION + "/person/transformPeople").asString(); + assertErrorResponse(HttpStatus.BAD_REQUEST_400, "Records to run through this process were not specified", response); + + response = Unirest.post(BASE_URL + "/api/" + VERSION + "/person/transformPeople?id=999").asString(); + assertEquals(HttpStatus.NO_CONTENT_204, response.getStatus()); + assertEquals("", response.getBody()); + + response = Unirest.post(BASE_URL + "/api/" + VERSION + "/person/transformPeople?id=1,2,3").asString(); + assertEquals(HttpStatus.MULTI_STATUS_207, response.getStatus()); + JSONArray jsonArray = new JSONArray(response.getBody()); + assertEquals(3, jsonArray.length()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testAsyncProcessAndGetStatus() throws QException + { + insertSimpsons(); + + HttpResponse response = Unirest.post(BASE_URL + "/api/" + VERSION + "/person/transformPeople?id=1,2,3&async=true").asString(); + assertEquals(HttpStatus.ACCEPTED_202, response.getStatus()); + JSONObject acceptedJSON = new JSONObject(response.getBody()); + String jobId = acceptedJSON.getString("jobId"); + assertNotNull(jobId); + + for(int i = 0; i < 10; i++) + { + SleepUtils.sleep(100, TimeUnit.MILLISECONDS); + + response = Unirest.get(BASE_URL + "/api/" + VERSION + "/person/transformPeople/status/" + jobId).asString(); + if(response.getStatus() == HttpStatus.MULTI_STATUS_207) + { + JSONArray jsonArray = new JSONArray(response.getBody()); + assertEquals(3, jsonArray.length()); + return; + } + } + + fail("Never got back a 207, after many sleeps"); + } + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/model/metadata/ApiInstanceMetaDataTest.java b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/model/metadata/ApiInstanceMetaDataTest.java index fcbb5ac7..bc2ac00e 100644 --- a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/model/metadata/ApiInstanceMetaDataTest.java +++ b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/model/metadata/ApiInstanceMetaDataTest.java @@ -114,11 +114,11 @@ class ApiInstanceMetaDataTest qInstance.addTable(new QTableMetaData() .withName("myValidTable") - .withMiddlewareMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData().withInitialVersion("2023.Q1")))); + .withSupplementalMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData().withInitialVersion("2023.Q1")))); qInstance.addTable(new QTableMetaData() .withName("myInvalidTable") - .withMiddlewareMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData().withInitialVersion("notAVersion")))); + .withSupplementalMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData().withInitialVersion("notAVersion")))); assertValidationErrors(qInstance, makeBaselineValidApiInstanceMetaData() .withCurrentVersion(new APIVersion("2023.Q1")) @@ -127,7 +127,7 @@ class ApiInstanceMetaDataTest qInstance.addTable(new QTableMetaData() .withName("myFutureValidTable") - .withMiddlewareMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData().withInitialVersion("2024.Q1")))); + .withSupplementalMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData().withInitialVersion("2024.Q1")))); assertValidationErrors(qInstance, makeBaselineValidApiInstanceMetaData() .withCurrentVersion(new APIVersion("2023.Q1")) @@ -195,7 +195,7 @@ class ApiInstanceMetaDataTest *******************************************************************************/ private void assertValidationErrors(QInstance qInstance, ApiInstanceMetaData apiInstanceMetaData, List expectedErrors) { - qInstance.withMiddlewareMetaData(new ApiInstanceMetaDataContainer().withApiInstanceMetaData(apiInstanceMetaData)); + qInstance.withSupplementalMetaData(new ApiInstanceMetaDataContainer().withApiInstanceMetaData(apiInstanceMetaData)); QInstanceValidator validator = new QInstanceValidator(); apiInstanceMetaData.validate(apiInstanceMetaData.getName(), qInstance, validator); diff --git a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/utils/ApiScriptUtilsTest.java b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/utils/ApiScriptUtilsTest.java index ec4cfb2b..fdc06e63 100644 --- a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/utils/ApiScriptUtilsTest.java +++ b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/utils/ApiScriptUtilsTest.java @@ -25,6 +25,7 @@ package com.kingsrook.qqq.api.utils; import java.io.Serializable; import java.util.List; import java.util.Map; +import java.util.concurrent.TimeUnit; import com.kingsrook.qqq.api.BaseTest; import com.kingsrook.qqq.api.TestUtils; import com.kingsrook.qqq.api.javalin.QBadRequestException; @@ -33,9 +34,12 @@ import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QNotFoundException; import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput; import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput; +import com.kingsrook.qqq.backend.core.utils.JsonUtils; +import com.kingsrook.qqq.backend.core.utils.SleepUtils; import com.kingsrook.qqq.backend.core.utils.ValueUtils; import org.junit.jupiter.api.Test; import static com.kingsrook.qqq.api.TestUtils.insertSimpsons; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -262,6 +266,87 @@ class ApiScriptUtilsTest extends BaseTest + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testGetProcessForObject() throws QException + { + ApiScriptUtils apiScriptUtils = newDefaultApiScriptUtils(); + + assertThatThrownBy(() -> apiScriptUtils.runProcess(TestUtils.PROCESS_NAME_GET_PERSON_INFO)) + .isInstanceOf(QBadRequestException.class) + .hasMessageContaining("Request failed with 4 reasons: Missing value for required input field"); + + Object result = apiScriptUtils.runProcess(TestUtils.PROCESS_NAME_GET_PERSON_INFO, """ + {"age": 43, "partnerPersonId": 1, "heightInches": 72, "weightPounds": 220, "homeTown": "Chester"} + """); + + assertThat(result).isInstanceOf(Map.class); + Map resultMap = (Map) result; + assertEquals(15695, resultMap.get("daysOld")); + assertEquals("Guy from Chester", resultMap.get("nickname")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testPostProcessForProcessSummaryList() throws QException + { + insertSimpsons(); + + ApiScriptUtils apiScriptUtils = newDefaultApiScriptUtils(); + + assertThatThrownBy(() -> apiScriptUtils.runProcess(TestUtils.PROCESS_NAME_TRANSFORM_PEOPLE, null)) + .isInstanceOf(QBadRequestException.class) + .hasMessageContaining("Records to run through this process were not specified"); + + Serializable emptyResult = apiScriptUtils.runProcess(TestUtils.PROCESS_NAME_TRANSFORM_PEOPLE, JsonUtils.toJson(Map.of("id", 999))); + assertThat(emptyResult).isInstanceOf(List.class); + assertEquals(0, ((List) emptyResult).size()); + + Serializable result = apiScriptUtils.runProcess(TestUtils.PROCESS_NAME_TRANSFORM_PEOPLE, JsonUtils.toJson(Map.of("id", "1,2,3"))); + assertThat(result).isInstanceOf(List.class); + List> resultList = (List>) result; + assertEquals(3, resultList.size()); + + assertThat(resultList.stream().filter(m -> m.get("id").equals(2)).findFirst()).isPresent().get().hasFieldOrPropertyWithValue("statusCode", 200); + assertThat(resultList.stream().filter(m -> m.get("id").equals(3)).findFirst()).isPresent().get().hasFieldOrPropertyWithValue("statusCode", 500); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testAsyncProcessAndGetStatus() throws QException + { + insertSimpsons(); + + ApiScriptUtils apiScriptUtils = newDefaultApiScriptUtils(); + + Serializable asyncResult = apiScriptUtils.runProcess(TestUtils.PROCESS_NAME_TRANSFORM_PEOPLE, JsonUtils.toJson(Map.of("id", "1,2,3", "async", true))); + assertThat(asyncResult).isInstanceOf(Map.class); + String jobId = ValueUtils.getValueAsString(((Map) asyncResult).get("jobId")); + assertNotNull(jobId); + + SleepUtils.sleep(100, TimeUnit.MILLISECONDS); + + Serializable result = apiScriptUtils.getProcessStatus(TestUtils.PROCESS_NAME_TRANSFORM_PEOPLE, jobId); + assertThat(result).isInstanceOf(List.class); + List> resultList = (List>) result; + assertEquals(3, resultList.size()); + + assertThat(resultList.stream().filter(m -> m.get("id").equals(2)).findFirst()).isPresent().get().hasFieldOrPropertyWithValue("statusCode", 200); + assertThat(resultList.stream().filter(m -> m.get("id").equals(3)).findFirst()).isPresent().get().hasFieldOrPropertyWithValue("statusCode", 500); + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java index aaf4dd0e..0245e235 100644 --- a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java +++ b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java @@ -66,6 +66,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportView; import com.kingsrook.qqq.backend.core.model.metadata.reporting.ReportType; import com.kingsrook.qqq.backend.core.model.metadata.tables.AssociatedScript; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.model.savedfilters.SavedFiltersMetaDataProvider; import com.kingsrook.qqq.backend.core.model.scripts.ScriptsMetaDataProvider; import com.kingsrook.qqq.backend.core.processes.implementations.mock.MockBackendStep; import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager; @@ -156,6 +157,7 @@ public class TestUtils qInstance.addBackend(defineMemoryBackend()); try { + new SavedFiltersMetaDataProvider().defineAll(qInstance, defineMemoryBackend().getName(), null); new ScriptsMetaDataProvider().defineAll(qInstance, defineMemoryBackend().getName(), null); } catch(Exception e) @@ -362,8 +364,7 @@ public class TestUtils .withType(QPossibleValueSourceType.TABLE) .withTableName(TABLE_NAME_PERSON) .withValueFormatAndFields(PVSValueFormatAndFields.LABEL_PARENS_ID) - .withOrderByField("id") - ); + .withOrderByField("id")); }