diff --git a/.gitignore b/.gitignore index 0310e0fb..dffbdad6 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ target/ *.iml .env +.idea ############################################# ## Original contents from github template: ## diff --git a/pom.xml b/pom.xml index de49e349..906f638a 100644 --- a/pom.xml +++ b/pom.xml @@ -33,6 +33,7 @@ qqq-backend-module-api qqq-backend-module-filesystem qqq-backend-module-rdbms + qqq-language-support-javascript qqq-middleware-picocli qqq-middleware-javalin qqq-middleware-lambda @@ -41,7 +42,7 @@ - 0.6.0-SNAPSHOT + 0.6.0 UTF-8 UTF-8 diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/QuickSightChartRenderer.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/QuickSightChartRenderer.java index 3defb074..1260ec09 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/QuickSightChartRenderer.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/QuickSightChartRenderer.java @@ -22,6 +22,7 @@ package com.kingsrook.qqq.backend.core.actions.dashboard; +import com.kingsrook.qqq.backend.core.actions.ActionHelper; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput; import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetOutput; @@ -51,6 +52,8 @@ public class QuickSightChartRenderer extends AbstractWidgetRenderer @Override public RenderWidgetOutput render(RenderWidgetInput input) throws QException { + ActionHelper.validateSession(input); + try { QuickSightChartMetaData quickSightMetaData = (QuickSightChartMetaData) input.getWidgetMetaData(); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/RenderWidgetAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/RenderWidgetAction.java index 00ba8fca..fe2ed2c1 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/RenderWidgetAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/RenderWidgetAction.java @@ -22,6 +22,7 @@ package com.kingsrook.qqq.backend.core.actions.dashboard; +import com.kingsrook.qqq.backend.core.actions.ActionHelper; import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput; @@ -40,6 +41,8 @@ public class RenderWidgetAction *******************************************************************************/ public RenderWidgetOutput execute(RenderWidgetInput input) throws QException { + ActionHelper.validateSession(input); + AbstractWidgetRenderer widgetRenderer = QCodeLoader.getAdHoc(AbstractWidgetRenderer.class, input.getWidgetMetaData().getCodeReference()); return (widgetRenderer.render(input)); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/interfaces/GetInterface.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/interfaces/GetInterface.java new file mode 100644 index 00000000..258cd856 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/interfaces/GetInterface.java @@ -0,0 +1,40 @@ +/* + * 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.actions.interfaces; + + +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput; + + +/******************************************************************************* + ** Interface for the Get action. + ** + *******************************************************************************/ +public interface GetInterface +{ + /******************************************************************************* + ** + *******************************************************************************/ + GetOutput execute(GetInput getInput) throws QException; +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/MetaDataAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/MetaDataAction.java index 7527556e..9cf25b4c 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/MetaDataAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/MetaDataAction.java @@ -30,6 +30,7 @@ import com.kingsrook.qqq.backend.core.actions.ActionHelper; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataInput; import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataOutput; +import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData; import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaDataInterface; import com.kingsrook.qqq.backend.core.model.metadata.frontend.AppTreeNode; import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendAppMetaData; @@ -69,8 +70,10 @@ public class MetaDataAction Map tables = new LinkedHashMap<>(); for(Map.Entry entry : metaDataInput.getInstance().getTables().entrySet()) { - tables.put(entry.getKey(), new QFrontendTableMetaData(entry.getValue(), false)); - treeNodes.put(entry.getKey(), new AppTreeNode(entry.getValue())); + String tableName = entry.getKey(); + QBackendMetaData backendForTable = metaDataInput.getInstance().getBackendForTable(tableName); + tables.put(tableName, new QFrontendTableMetaData(backendForTable, entry.getValue(), false)); + treeNodes.put(tableName, new AppTreeNode(entry.getValue())); } metaDataOutput.setTables(tables); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/TableMetaDataAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/TableMetaDataAction.java index 62623bdd..d67ec855 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/TableMetaDataAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/TableMetaDataAction.java @@ -27,6 +27,7 @@ import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QNotFoundException; import com.kingsrook.qqq.backend.core.model.actions.metadata.TableMetaDataInput; import com.kingsrook.qqq.backend.core.model.actions.metadata.TableMetaDataOutput; +import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData; import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendTableMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; @@ -52,7 +53,8 @@ public class TableMetaDataAction { throw (new QNotFoundException("Table [" + tableMetaDataInput.getTableName() + "] was not found.")); } - tableMetaDataOutput.setTable(new QFrontendTableMetaData(table, true)); + QBackendMetaData backendForTable = tableMetaDataInput.getInstance().getBackendForTable(table.getName()); + tableMetaDataOutput.setTable(new QFrontendTableMetaData(backendForTable, table, true)); // todo post-customization - can do whatever w/ the result if you want diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/processes/RunProcessAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/processes/RunProcessAction.java index 562f2d8a..34e33336 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/processes/RunProcessAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/processes/RunProcessAction.java @@ -23,26 +23,43 @@ package com.kingsrook.qqq.backend.core.actions.processes; import java.io.Serializable; +import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.UUID; import com.kingsrook.qqq.backend.core.actions.ActionHelper; +import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; +import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; +import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessState; 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.processes.RunProcessInput; import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput; +import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; +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.actions.tables.query.QueryInput; +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.data.QRecord; import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData; 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.processes.implementations.basepull.BasepullConfiguration; 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.CollectionUtils; +import com.kingsrook.qqq.backend.core.utils.ValueUtils; +import org.apache.commons.lang.BooleanUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -55,6 +72,15 @@ public class RunProcessAction { private static final Logger LOG = LogManager.getLogger(RunProcessAction.class); + public static final String BASEPULL_THIS_RUNTIME_KEY = "basepullThisRuntimeKey"; + public static final String BASEPULL_LAST_RUNTIME_KEY = "basepullLastRuntimeKey"; + public static final String BASEPULL_TIMESTAMP_FIELD = "basepullTimestampField"; + + //////////////////////////////////////////////////////////////////////////////////////////////// + // indicator that the timestamp field should be updated - e.g., the execute step is finished. // + //////////////////////////////////////////////////////////////////////////////////////////////// + public static final String BASEPULL_READY_TO_UPDATE_TIMESTAMP_FIELD = "basepullReadyToUpdateTimestamp"; + /******************************************************************************* @@ -84,6 +110,18 @@ public class RunProcessAction UUIDAndTypeStateKey stateKey = new UUIDAndTypeStateKey(UUID.fromString(runProcessInput.getProcessUUID()), StateType.PROCESS_STATUS); ProcessState processState = primeProcessState(runProcessInput, stateKey, process); + ///////////////////////////////////////////////////////// + // if process is 'basepull' style, keep track of 'now' // + ///////////////////////////////////////////////////////// + BasepullConfiguration basepullConfiguration = process.getBasepullConfiguration(); + if(basepullConfiguration != null) + { + /////////////////////////////////////// + // get the stored basepull timestamp // + /////////////////////////////////////// + persistLastRunTime(runProcessInput, process, basepullConfiguration); + } + try { String lastStepName = runProcessInput.getStartAfterStep(); @@ -151,6 +189,15 @@ public class RunProcessAction throw (new QException("Unsure how to run a step of type: " + step.getClass().getName())); } } + + //////////////////////////////////////////////////////////////////////////////////// + // if 'basepull' style process, update the stored basepull timestamp // + // but only when we've been signaled to do so - i.e., after an Execute step runs. // + //////////////////////////////////////////////////////////////////////////////////// + if(basepullConfiguration != null && BooleanUtils.isTrue(ValueUtils.getValueAsBoolean(runProcessInput.getValue(BASEPULL_READY_TO_UPDATE_TIMESTAMP_FIELD)))) + { + storeLastRunTime(runProcessInput, process, basepullConfiguration); + } } catch(QException qe) { @@ -250,7 +297,17 @@ public class RunProcessAction runBackendStepInput.setTableName(process.getTableName()); runBackendStepInput.setSession(runProcessInput.getSession()); runBackendStepInput.setCallback(runProcessInput.getCallback()); + runBackendStepInput.setFrontendStepBehavior(runProcessInput.getFrontendStepBehavior()); runBackendStepInput.setAsyncJobCallback(runProcessInput.getAsyncJobCallback()); + + /////////////////////////////////////////////////////////////// + // if 'basepull' values are in the inputs, add to step input // + /////////////////////////////////////////////////////////////// + if(runProcessInput.getValues().containsKey(BASEPULL_LAST_RUNTIME_KEY)) + { + runBackendStepInput.setBasepullLastRunTime((Instant) runProcessInput.getValues().get(BASEPULL_LAST_RUNTIME_KEY)); + } + RunBackendStepOutput lastFunctionResult = new RunBackendStepAction().execute(runBackendStepInput); storeState(stateKey, lastFunctionResult.getProcessState()); @@ -368,4 +425,129 @@ public class RunProcessAction return (getStateProvider().get(ProcessState.class, stateKey)); } + + + /******************************************************************************* + ** Insert or update the last runtime value for this basepull into the backend. + *******************************************************************************/ + protected void storeLastRunTime(RunProcessInput runProcessInput, QProcessMetaData process, BasepullConfiguration basepullConfiguration) throws QException + { + String basepullTableName = basepullConfiguration.getTableName(); + String basepullKeyFieldName = basepullConfiguration.getKeyField(); + String basepullLastRunTimeFieldName = basepullConfiguration.getLastRunTimeFieldName(); + String basepullKeyValue = (basepullConfiguration.getKeyValue() != null) ? basepullConfiguration.getKeyValue() : process.getName(); + + /////////////////////////////////////// + // get the stored basepull timestamp // + /////////////////////////////////////// + QueryInput queryInput = new QueryInput(runProcessInput.getInstance()); + queryInput.setSession(runProcessInput.getSession()); + queryInput.setTableName(basepullTableName); + queryInput.setFilter(new QQueryFilter().withCriteria( + new QFilterCriteria() + .withFieldName(basepullKeyFieldName) + .withOperator(QCriteriaOperator.EQUALS) + .withValues(List.of(basepullKeyValue)))); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + + ////////////////////////////////////////// + // get the runtime for this process run // + ////////////////////////////////////////// + Instant newRunTime = (Instant) runProcessInput.getValues().get(BASEPULL_THIS_RUNTIME_KEY); + + ///////////////////////////////////////////////// + // update if found, otherwise insert new value // + ///////////////////////////////////////////////// + if(CollectionUtils.nullSafeHasContents(queryOutput.getRecords())) + { + /////////////////////////////////////////////////////////////////////////////// + // update the basepull table with 'now' (which is before original query ran) // + /////////////////////////////////////////////////////////////////////////////// + QRecord basepullRecord = queryOutput.getRecords().get(0); + basepullRecord.setValue(basepullLastRunTimeFieldName, newRunTime); + + //////////// + // update // + //////////// + UpdateInput updateInput = new UpdateInput(runProcessInput.getInstance()); + updateInput.setSession(runProcessInput.getSession()); + updateInput.setTableName(basepullTableName); + updateInput.setRecords(List.of(basepullRecord)); + new UpdateAction().execute(updateInput); + } + else + { + QRecord basepullRecord = new QRecord() + .withValue(basepullKeyFieldName, basepullKeyValue) + .withValue(basepullLastRunTimeFieldName, newRunTime); + + //////////////////////////////// + // insert new basepull record // + //////////////////////////////// + InsertInput insertInput = new InsertInput(runProcessInput.getInstance()); + insertInput.setSession(runProcessInput.getSession()); + insertInput.setTableName(basepullTableName); + insertInput.setRecords(List.of(basepullRecord)); + new InsertAction().execute(insertInput); + } + } + + + + /******************************************************************************* + ** Lookup the last runtime for this basepull, and set it (plus now) in the process's + ** values. + *******************************************************************************/ + protected void persistLastRunTime(RunProcessInput runProcessInput, QProcessMetaData process, BasepullConfiguration basepullConfiguration) throws QException + { + //////////////////////////////////////////////////////// + // if these values were already computed, don't re-do // + //////////////////////////////////////////////////////// + if(runProcessInput.getValue(BASEPULL_THIS_RUNTIME_KEY) != null) + { + return; + } + + ///////////////////////////////////////////////////////////////////////////////////////////////// + // store 'now', which will be used to update basepull record if process completes successfully // + ///////////////////////////////////////////////////////////////////////////////////////////////// + Instant now = Instant.now(); + runProcessInput.getValues().put(BASEPULL_THIS_RUNTIME_KEY, now); + + String basepullTableName = basepullConfiguration.getTableName(); + String basepullKeyFieldName = basepullConfiguration.getKeyField(); + String basepullLastRunTimeFieldName = basepullConfiguration.getLastRunTimeFieldName(); + Integer basepullHoursBackForInitialTimestamp = basepullConfiguration.getHoursBackForInitialTimestamp(); + String basepullKeyValue = (basepullConfiguration.getKeyValue() != null) ? basepullConfiguration.getKeyValue() : process.getName(); + + /////////////////////////////////////// + // get the stored basepull timestamp // + /////////////////////////////////////// + QueryInput queryInput = new QueryInput(runProcessInput.getInstance()); + queryInput.setSession(runProcessInput.getSession()); + queryInput.setTableName(basepullTableName); + queryInput.setFilter(new QQueryFilter().withCriteria( + new QFilterCriteria() + .withFieldName(basepullKeyFieldName) + .withOperator(QCriteriaOperator.EQUALS) + .withValues(List.of(basepullKeyValue)))); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + + /////////////////////////////////////////////////////////////////////////////////////////////////// + // get the stored time, if not, default to 'now' unless a number of hours to offset was provided // + /////////////////////////////////////////////////////////////////////////////////////////////////// + Instant lastRunTime = now; + if(CollectionUtils.nullSafeHasContents(queryOutput.getRecords())) + { + QRecord basepullRecord = queryOutput.getRecords().get(0); + lastRunTime = ValueUtils.getValueAsInstant(basepullRecord.getValue(basepullLastRunTimeFieldName)); + } + else if(basepullHoursBackForInitialTimestamp != null) + { + lastRunTime = lastRunTime.minus(basepullHoursBackForInitialTimestamp, ChronoUnit.HOURS); + } + + runProcessInput.getValues().put(BASEPULL_LAST_RUNTIME_KEY, lastRunTime); + runProcessInput.getValues().put(BASEPULL_TIMESTAMP_FIELD, basepullConfiguration.getTimestampField()); + } } 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 new file mode 100644 index 00000000..44db3df9 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/ExecuteCodeAction.java @@ -0,0 +1,119 @@ +/* + * 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.actions.scripts; + + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; +import com.kingsrook.qqq.backend.core.actions.scripts.logging.Log4jCodeExecutionLogger; +import com.kingsrook.qqq.backend.core.actions.scripts.logging.QCodeExecutionLoggerInterface; +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; +import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeOutput; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; + + +/******************************************************************************* + ** Action to execute user/runtime defined code. + ** + ** This action is designed to support code in multiple languages, by using + ** executors, e.g., provided by additional runtime qqq dependencies. Initially + ** we are building qqq-language-support-javascript. + ** + ** We also have a Java executor, to provide at least a little bit of testability + ** within qqq-backend-core. This executor is a candidate to be replaced in the + ** future with something that would do actual dynamic java (whether that's compiled + ** at runtime, or loaded from a plugin jar at runtime). In other words, the java + ** executor in place today is just meant to be a placeholder. + *******************************************************************************/ +public class ExecuteCodeAction +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @SuppressWarnings("checkstyle:indentation") + public void run(ExecuteCodeInput input, ExecuteCodeOutput output) throws QException, QCodeException + { + QCodeReference codeReference = input.getCodeReference(); + + QCodeExecutionLoggerInterface executionLogger = input.getExecutionLogger(); + if(executionLogger == null) + { + executionLogger = getDefaultExecutionLogger(); + } + executionLogger.acceptExecutionStart(input); + + try + { + String languageExecutor = switch(codeReference.getCodeType()) + { + case JAVA -> "com.kingsrook.qqq.backend.core.actions.scripts.QJavaExecutor"; + case JAVA_SCRIPT -> "com.kingsrook.qqq.languages.javascript.QJavaScriptExecutor"; + }; + + @SuppressWarnings("unchecked") + Class executorClass = (Class) Class.forName(languageExecutor); + QCodeExecutor qCodeExecutor = executorClass.getConstructor().newInstance(); + + //////////////////////////////////////////////////////////////////////////////////////////////////// + // merge all of the input context, plus the input... input - into a context for the code executor // + //////////////////////////////////////////////////////////////////////////////////////////////////// + Map context = new HashMap<>(); + if(input.getContext() != null) + { + context.putAll(input.getContext()); + } + if(input.getInput() != null) + { + context.putAll(input.getInput()); + } + + Serializable codeOutput = qCodeExecutor.execute(codeReference, context, executionLogger); + output.setOutput(codeOutput); + executionLogger.acceptExecutionEnd(codeOutput); + } + catch(QCodeException qCodeException) + { + executionLogger.acceptException(qCodeException); + throw (qCodeException); + } + catch(Exception e) + { + executionLogger.acceptException(e); + throw (new QException("Error executing code [" + codeReference + "]", e)); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private QCodeExecutionLoggerInterface getDefaultExecutionLogger() + { + return (new Log4jCodeExecutionLogger()); + } + +} 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 new file mode 100644 index 00000000..e6cd808a --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/QCodeExecutor.java @@ -0,0 +1,44 @@ +/* + * 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.actions.scripts; + + +import java.io.Serializable; +import java.util.Map; +import com.kingsrook.qqq.backend.core.actions.scripts.logging.QCodeExecutionLoggerInterface; +import com.kingsrook.qqq.backend.core.exceptions.QCodeException; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; + + +/******************************************************************************* + ** Interface to be implemented by language-specific code executors, e.g., in + ** qqq-language-support-${languageName} maven modules. + *******************************************************************************/ +public interface QCodeExecutor +{ + + /******************************************************************************* + ** + *******************************************************************************/ + Serializable execute(QCodeReference codeReference, Map inputContext, QCodeExecutionLoggerInterface executionLogger) throws QCodeException; + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/QJavaExecutor.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/QJavaExecutor.java new file mode 100644 index 00000000..61451b38 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/QJavaExecutor.java @@ -0,0 +1,68 @@ +/* + * 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.actions.scripts; + + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; +import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader; +import com.kingsrook.qqq.backend.core.actions.scripts.logging.QCodeExecutionLoggerInterface; +import com.kingsrook.qqq.backend.core.exceptions.QCodeException; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; + + +/******************************************************************************* + ** Java + *******************************************************************************/ +public class QJavaExecutor implements QCodeExecutor +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public Serializable execute(QCodeReference codeReference, Map inputContext, QCodeExecutionLoggerInterface executionLogger) throws QCodeException + { + Map context = new HashMap<>(inputContext); + if(!context.containsKey("logger")) + { + context.put("logger", executionLogger); + } + + Serializable output; + try + { + Function, Serializable> function = QCodeLoader.getFunction(codeReference); + output = function.apply(context); + } + catch(Exception e) + { + QCodeException qCodeException = new QCodeException("Error executing script", e); + throw (qCodeException); + } + + return (output); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/RunAssociatedScriptAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/RunAssociatedScriptAction.java new file mode 100644 index 00000000..9a88b15a --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/RunAssociatedScriptAction.java @@ -0,0 +1,152 @@ +/* + * 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.actions.scripts; + + +import java.io.Serializable; +import java.util.HashMap; +import com.kingsrook.qqq.backend.core.actions.ActionHelper; +import com.kingsrook.qqq.backend.core.actions.scripts.logging.StoreScriptLogAndScriptLogLineExecutionLogger; +import com.kingsrook.qqq.backend.core.actions.tables.GetAction; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.exceptions.QNotFoundException; +import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeInput; +import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeOutput; +import com.kingsrook.qqq.backend.core.model.actions.scripts.RunAssociatedScriptInput; +import com.kingsrook.qqq.backend.core.model.actions.scripts.RunAssociatedScriptOutput; +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.metadata.code.QCodeReference; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType; +import com.kingsrook.qqq.backend.core.model.scripts.Script; +import com.kingsrook.qqq.backend.core.model.scripts.ScriptRevision; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class RunAssociatedScriptAction +{ + + /******************************************************************************* + ** + *******************************************************************************/ + public void run(RunAssociatedScriptInput input, RunAssociatedScriptOutput output) throws QException + { + ActionHelper.validateSession(input); + + Serializable scriptId = getScriptId(input); + if(scriptId == null) + { + throw (new QNotFoundException("The input record [" + input.getCodeReference().getRecordTable() + "][" + input.getCodeReference().getRecordPrimaryKey() + + "] does not have a script specified for [" + input.getCodeReference().getFieldName() + "]")); + } + + Script script = getScript(input, scriptId); + if(script.getCurrentScriptRevisionId() == null) + { + throw (new QNotFoundException("The script for record [" + input.getCodeReference().getRecordTable() + "][" + input.getCodeReference().getRecordPrimaryKey() + + "] (scriptId=" + scriptId + ") does not have a current version.")); + } + + ScriptRevision scriptRevision = getCurrentScriptRevision(input, script.getCurrentScriptRevisionId()); + + ExecuteCodeInput executeCodeInput = new ExecuteCodeInput(input.getInstance()); + executeCodeInput.setSession(input.getSession()); + executeCodeInput.setInput(new HashMap<>(input.getInputValues())); + executeCodeInput.setContext(new HashMap<>()); + if(input.getOutputObject() != null) + { + executeCodeInput.getContext().put("output", input.getOutputObject()); + } + executeCodeInput.setCodeReference(new QCodeReference().withInlineCode(scriptRevision.getContents()).withCodeType(QCodeType.JAVA_SCRIPT)); // todo - code type as attribute of script!! + executeCodeInput.setExecutionLogger(new StoreScriptLogAndScriptLogLineExecutionLogger(scriptRevision.getScriptId(), scriptRevision.getId())); + ExecuteCodeOutput executeCodeOutput = new ExecuteCodeOutput(); + new ExecuteCodeAction().run(executeCodeInput, executeCodeOutput); + + output.setOutput(executeCodeOutput.getOutput()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private ScriptRevision getCurrentScriptRevision(RunAssociatedScriptInput input, Serializable scriptRevisionId) throws QException + { + GetInput getInput = new GetInput(input.getInstance()); + getInput.setSession(input.getSession()); + getInput.setTableName("scriptRevision"); + getInput.setPrimaryKey(scriptRevisionId); + GetOutput getOutput = new GetAction().execute(getInput); + if(getOutput.getRecord() == null) + { + throw (new QNotFoundException("The current revision of the script for record [" + input.getCodeReference().getRecordTable() + "][" + input.getCodeReference().getRecordPrimaryKey() + "][" + + input.getCodeReference().getFieldName() + "] (scriptRevisionId=" + scriptRevisionId + ") was not found.")); + } + + return (new ScriptRevision(getOutput.getRecord())); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private Script getScript(RunAssociatedScriptInput input, Serializable scriptId) throws QException + { + GetInput getInput = new GetInput(input.getInstance()); + getInput.setSession(input.getSession()); + getInput.setTableName("script"); + getInput.setPrimaryKey(scriptId); + GetOutput getOutput = new GetAction().execute(getInput); + + if(getOutput.getRecord() == null) + { + throw (new QNotFoundException("The script for record [" + input.getCodeReference().getRecordTable() + "][" + input.getCodeReference().getRecordPrimaryKey() + "][" + + input.getCodeReference().getFieldName() + "] (script id=" + scriptId + ") was not found.")); + } + + return (new Script(getOutput.getRecord())); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private Serializable getScriptId(RunAssociatedScriptInput input) throws QException + { + GetInput getInput = new GetInput(input.getInstance()); + getInput.setSession(input.getSession()); + getInput.setTableName(input.getCodeReference().getRecordTable()); + getInput.setPrimaryKey(input.getCodeReference().getRecordPrimaryKey()); + GetOutput getOutput = new GetAction().execute(getInput); + if(getOutput.getRecord() == null) + { + throw (new QNotFoundException("The requested record [" + input.getCodeReference().getRecordTable() + "][" + input.getCodeReference().getRecordPrimaryKey() + "] was not found.")); + } + + return (getOutput.getRecord().getValue(input.getCodeReference().getFieldName())); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/StoreAssociatedScriptAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/StoreAssociatedScriptAction.java new file mode 100644 index 00000000..c4bfe711 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/StoreAssociatedScriptAction.java @@ -0,0 +1,222 @@ +/* + * 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.actions.scripts; + + +import java.io.Serializable; +import java.util.List; +import java.util.Optional; +import com.kingsrook.qqq.backend.core.actions.ActionHelper; +import com.kingsrook.qqq.backend.core.actions.tables.GetAction; +import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; +import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; +import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.scripts.StoreAssociatedScriptInput; +import com.kingsrook.qqq.backend.core.model.actions.scripts.StoreAssociatedScriptOutput; +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.insert.InsertInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput; +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.actions.tables.update.UpdateInput; +import com.kingsrook.qqq.backend.core.model.data.QRecord; +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.utils.StringUtils; + + +/******************************************************************************* + ** Action to store a new version of a script, associated with a record. + ** + ** If there's never been a script assigned to the record (for the specified field), + ** then a new Script record is first inserted. + ** + ** The script referenced by the record is always updated to point at the new + ** scriptRevision record that is inserted. + ** + *******************************************************************************/ +public class StoreAssociatedScriptAction +{ + + /******************************************************************************* + ** + *******************************************************************************/ + public void run(StoreAssociatedScriptInput input, StoreAssociatedScriptOutput output) throws QException + { + ActionHelper.validateSession(input); + + QTableMetaData table = input.getTable(); + Optional optAssociatedScript = table.getAssociatedScripts().stream().filter(as -> as.getFieldName().equals(input.getFieldName())).findFirst(); + if(optAssociatedScript.isEmpty()) + { + throw (new QException("Field to update associated script for is not an associated script field.")); + } + AssociatedScript associatedScript = optAssociatedScript.get(); + + ///////////////////////////////////////////////////////////// + // get the record that the script is to be associated with // + ///////////////////////////////////////////////////////////// + QRecord associatedRecord; + { + GetInput getInput = new GetInput(input.getInstance()); + getInput.setSession(input.getSession()); + getInput.setTableName(input.getTableName()); + getInput.setPrimaryKey(input.getRecordPrimaryKey()); + getInput.setShouldGenerateDisplayValues(true); + GetOutput getOutput = new GetAction().execute(getInput); + associatedRecord = getOutput.getRecord(); + } + if(associatedRecord == null) + { + throw (new QException("Record to associated with script was not found.")); + } + + ////////////////////////////////////////////////////////////////// + // check if there's currently a script referenced by the record // + ////////////////////////////////////////////////////////////////// + Serializable existingScriptId = associatedRecord.getValueString(input.getFieldName()); + QRecord script; + Integer nextSequenceNo = 1; + if(existingScriptId == null) + { + //////////////////////////////////////////////////////////////////// + // get the script type - that'll be part of the new script's name // + //////////////////////////////////////////////////////////////////// + GetInput getInput = new GetInput(input.getInstance()); + getInput.setSession(input.getSession()); + getInput.setTableName("scriptType"); + getInput.setPrimaryKey(associatedScript.getScriptTypeId()); + getInput.setShouldGenerateDisplayValues(true); + GetOutput getOutput = new GetAction().execute(getInput); + QRecord scriptType = getOutput.getRecord(); + if(scriptType == null) + { + throw (new QException("Script type [" + associatedScript.getScriptTypeId() + "] was not found.")); + } + + ///////////////////////// + // insert a new script // + ///////////////////////// + script = new QRecord(); + script.setValue("scriptTypeId", associatedScript.getScriptTypeId()); + script.setValue("name", associatedRecord.getRecordLabel() + " - " + scriptType.getRecordLabel()); + InsertInput insertInput = new InsertInput(input.getInstance()); + insertInput.setSession(input.getSession()); + insertInput.setTableName("script"); + insertInput.setRecords(List.of(script)); + InsertOutput insertOutput = new InsertAction().execute(insertInput); + script = insertOutput.getRecords().get(0); + + ///////////////////////////////////////////////////////////// + // update the associated record to point at the new script // + ///////////////////////////////////////////////////////////// + UpdateInput updateInput = new UpdateInput(input.getInstance()); + updateInput.setSession(input.getSession()); + updateInput.setTableName(input.getTableName()); + updateInput.setRecords(List.of(new QRecord() + .withValue(table.getPrimaryKeyField(), associatedRecord.getValue(table.getPrimaryKeyField())) + .withValue(input.getFieldName(), script.getValue("id")) + )); + new UpdateAction().execute(updateInput); + } + else + { + //////////////////////////////////////// + // get the existing script, to update // + //////////////////////////////////////// + GetInput getInput = new GetInput(input.getInstance()); + getInput.setSession(input.getSession()); + getInput.setTableName("script"); + getInput.setPrimaryKey(existingScriptId); + GetOutput getOutput = new GetAction().execute(getInput); + script = getOutput.getRecord(); + + QueryInput queryInput = new QueryInput(input.getInstance()); + queryInput.setSession(input.getSession()); + queryInput.setTableName("scriptRevision"); + queryInput.setFilter(new QQueryFilter() + .withCriteria(new QFilterCriteria("scriptId", QCriteriaOperator.EQUALS, List.of(script.getValue("id")))) + .withOrderBy(new QFilterOrderBy("sequenceNo", false)) + ); + queryInput.setLimit(1); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + if(!queryOutput.getRecords().isEmpty()) + { + nextSequenceNo = queryOutput.getRecords().get(0).getValueInteger("sequenceNo") + 1; + } + } + + ////////////////////////////////// + // insert a new script revision // + ////////////////////////////////// + String commitMessage = input.getCommitMessage(); + if(!StringUtils.hasContent(commitMessage)) + { + if(nextSequenceNo == 1) + { + commitMessage = "Initial version"; + } + else + { + commitMessage = "No commit message given"; + } + } + + QRecord scriptRevision = new QRecord() + .withValue("scriptId", script.getValue("id")) + .withValue("contents", input.getCode()) + .withValue("commitMessage", commitMessage) + .withValue("sequenceNo", nextSequenceNo); + + try + { + scriptRevision.setValue("author", input.getSession().getUser().getFullName()); + } + catch(Exception e) + { + scriptRevision.setValue("author", "Unknown"); + } + + InsertInput insertInput = new InsertInput(input.getInstance()); + insertInput.setSession(input.getSession()); + insertInput.setTableName("scriptRevision"); + insertInput.setRecords(List.of(scriptRevision)); + InsertOutput insertOutput = new InsertAction().execute(insertInput); + scriptRevision = insertOutput.getRecords().get(0); + + //////////////////////////////////////////////////// + // update the script to point at the new revision // + //////////////////////////////////////////////////// + script.setValue("currentScriptRevisionId", scriptRevision.getValue("id")); + UpdateInput updateInput = new UpdateInput(input.getInstance()); + updateInput.setSession(input.getSession()); + updateInput.setTableName("script"); + updateInput.setRecords(List.of(script)); + new UpdateAction().execute(updateInput); + } +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/TestScriptAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/TestScriptAction.java new file mode 100644 index 00000000..7e9e1e5d --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/TestScriptAction.java @@ -0,0 +1,65 @@ +/* + * 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.actions.scripts; + + +import java.util.HashMap; +import com.kingsrook.qqq.backend.core.actions.scripts.logging.BuildScriptLogAndScriptLogLineExecutionLogger; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeInput; +import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeOutput; +import com.kingsrook.qqq.backend.core.model.actions.scripts.TestScriptInput; +import com.kingsrook.qqq.backend.core.model.actions.scripts.TestScriptOutput; +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.tables.QTableMetaData; + + +/******************************************************************************* + ** Class for running a test of a script - e.g., maybe before it is saved. + *******************************************************************************/ +public class TestScriptAction +{ + + /******************************************************************************* + ** + *******************************************************************************/ + public void run(TestScriptInput input, TestScriptOutput output) throws QException + { + QTableMetaData table = input.getTable(); + + ExecuteCodeInput executeCodeInput = new ExecuteCodeInput(input.getInstance()); + executeCodeInput.setSession(input.getSession()); + executeCodeInput.setInput(new HashMap<>(input.getInputValues())); + executeCodeInput.setContext(new HashMap<>()); + // todo! if(input.getOutputObject() != null) + // todo! { + // todo! executeCodeInput.getContext().put("output", input.getOutputObject()); + // todo! } + executeCodeInput.setCodeReference(new QCodeReference().withInlineCode(input.getCode()).withCodeType(QCodeType.JAVA_SCRIPT)); // todo - code type as attribute of script!! + executeCodeInput.setExecutionLogger(new BuildScriptLogAndScriptLogLineExecutionLogger()); + ExecuteCodeOutput executeCodeOutput = new ExecuteCodeOutput(); + new ExecuteCodeAction().run(executeCodeInput, executeCodeOutput); + + // todo! output.setOutput(executeCodeOutput.getOutput()); + } +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/logging/BuildScriptLogAndScriptLogLineExecutionLogger.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/logging/BuildScriptLogAndScriptLogLineExecutionLogger.java new file mode 100644 index 00000000..7dc4b45e --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/logging/BuildScriptLogAndScriptLogLineExecutionLogger.java @@ -0,0 +1,221 @@ +/* + * 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.actions.scripts.logging; + + +import java.io.Serializable; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.List; +import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeInput; +import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.utils.StringUtils; +import com.kingsrook.qqq.backend.core.utils.ValueUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + + +/******************************************************************************* + ** Implementation of a code execution logger that builds a scriptLog and 0 or more + ** scriptLogLine records - but doesn't insert them. e.g., useful for testing + ** (both in junit, and for users in-app). + *******************************************************************************/ +public class BuildScriptLogAndScriptLogLineExecutionLogger implements QCodeExecutionLoggerInterface +{ + private static final Logger LOG = LogManager.getLogger(BuildScriptLogAndScriptLogLineExecutionLogger.class); + + private QRecord scriptLog; + private List scriptLogLines = new ArrayList<>(); + protected ExecuteCodeInput executeCodeInput; + + private Serializable scriptId; + private Serializable scriptRevisionId; + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public BuildScriptLogAndScriptLogLineExecutionLogger() + { + } + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public BuildScriptLogAndScriptLogLineExecutionLogger(Serializable scriptId, Serializable scriptRevisionId) + { + this.scriptId = scriptId; + this.scriptRevisionId = scriptRevisionId; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private QRecord buildHeaderRecord(ExecuteCodeInput executeCodeInput) + { + return (new QRecord() + .withValue("scriptId", scriptId) + .withValue("scriptRevisionId", scriptRevisionId) + .withValue("startTimestamp", Instant.now()) + .withValue("input", truncate(executeCodeInput.getInput()))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + protected QRecord buildDetailLogRecord(String logLine) + { + return (new QRecord() + .withValue("scriptLogId", scriptLog.getValue("id")) + .withValue("timestamp", Instant.now()) + .withValue("text", truncate(logLine))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private String truncate(Object o) + { + return StringUtils.safeTruncate(ValueUtils.getValueAsString(o), 1000, "..."); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + protected void updateHeaderAtEnd(Serializable output, Exception exception) + { + Instant startTimestamp = (Instant) scriptLog.getValue("startTimestamp"); + Instant endTimestamp = Instant.now(); + scriptLog.setValue("endTimestamp", endTimestamp); + scriptLog.setValue("runTimeMillis", startTimestamp.until(endTimestamp, ChronoUnit.MILLIS)); + + if(exception != null) + { + scriptLog.setValue("hadError", true); + scriptLog.setValue("error", exception.getMessage()); + } + else + { + scriptLog.setValue("hadError", false); + scriptLog.setValue("output", truncate(output)); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void acceptExecutionStart(ExecuteCodeInput executeCodeInput) + { + try + { + this.executeCodeInput = executeCodeInput; + this.scriptLog = buildHeaderRecord(executeCodeInput); + } + catch(Exception e) + { + LOG.warn("Error starting storage of script log", e); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void acceptLogLine(String logLine) + { + scriptLogLines.add(buildDetailLogRecord(logLine)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void acceptException(Exception exception) + { + updateHeaderAtEnd(null, exception); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void acceptExecutionEnd(Serializable output) + { + updateHeaderAtEnd(output, null); + } + + + + /******************************************************************************* + ** Getter for scriptLog + ** + *******************************************************************************/ + public QRecord getScriptLog() + { + return scriptLog; + } + + + + /******************************************************************************* + ** Getter for scriptLogLines + ** + *******************************************************************************/ + public List getScriptLogLines() + { + return scriptLogLines; + } + + + + /******************************************************************************* + ** Setter for scriptLog + ** + *******************************************************************************/ + protected void setScriptLog(QRecord scriptLog) + { + this.scriptLog = scriptLog; + } +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/logging/Log4jCodeExecutionLogger.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/logging/Log4jCodeExecutionLogger.java new file mode 100644 index 00000000..498652ef --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/logging/Log4jCodeExecutionLogger.java @@ -0,0 +1,93 @@ +/* + * 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.actions.scripts.logging; + + +import java.io.Serializable; +import java.util.UUID; +import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeInput; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; +import com.kingsrook.qqq.backend.core.utils.StringUtils; +import com.kingsrook.qqq.backend.core.utils.ValueUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + + +/******************************************************************************* + ** Implementation of a code execution logger that logs to LOG 4j + *******************************************************************************/ +public class Log4jCodeExecutionLogger implements QCodeExecutionLoggerInterface +{ + private static final Logger LOG = LogManager.getLogger(Log4jCodeExecutionLogger.class); + + private QCodeReference qCodeReference; + private String uuid = UUID.randomUUID().toString(); + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void acceptExecutionStart(ExecuteCodeInput executeCodeInput) + { + this.qCodeReference = executeCodeInput.getCodeReference(); + + String inputString = StringUtils.safeTruncate(ValueUtils.getValueAsString(executeCodeInput.getInput()), 250, "..."); + LOG.info("Starting script execution: " + qCodeReference.getName() + ", uuid: " + uuid + ", with input: " + inputString); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void acceptLogLine(String logLine) + { + LOG.info("Script log: " + uuid + ": " + logLine); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void acceptException(Exception exception) + { + LOG.info("Script Exception: " + uuid, exception); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void acceptExecutionEnd(Serializable output) + { + String outputString = StringUtils.safeTruncate(ValueUtils.getValueAsString(output), 250, "..."); + LOG.info("Finished script execution: " + qCodeReference.getName() + ", uuid: " + uuid + ", with output: " + outputString); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/logging/NoopCodeExecutionLogger.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/logging/NoopCodeExecutionLogger.java new file mode 100644 index 00000000..d2e9e56d --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/logging/NoopCodeExecutionLogger.java @@ -0,0 +1,74 @@ +/* + * 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.actions.scripts.logging; + + +import java.io.Serializable; +import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeInput; + + +/******************************************************************************* + ** Implementation of a code execution logger that just noop's every action. + *******************************************************************************/ +public class NoopCodeExecutionLogger implements QCodeExecutionLoggerInterface +{ + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void acceptExecutionStart(ExecuteCodeInput executeCodeInput) + { + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void acceptLogLine(String logLine) + { + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void acceptException(Exception exception) + { + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void acceptExecutionEnd(Serializable output) + { + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/logging/QCodeExecutionLoggerInterface.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/logging/QCodeExecutionLoggerInterface.java new file mode 100644 index 00000000..5277084f --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/logging/QCodeExecutionLoggerInterface.java @@ -0,0 +1,64 @@ +/* + * 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.actions.scripts.logging; + + +import java.io.Serializable; +import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeInput; + + +/******************************************************************************* + ** Interface to provide logging functionality to QCodeExecution (e.g., scripts) + *******************************************************************************/ +public interface QCodeExecutionLoggerInterface +{ + + /******************************************************************************* + ** Called when the execution starts - takes the execution's input object. + *******************************************************************************/ + void acceptExecutionStart(ExecuteCodeInput executeCodeInput); + + /******************************************************************************* + ** Called to log a line, a message. + *******************************************************************************/ + void acceptLogLine(String logLine); + + /******************************************************************************* + ** In case the loggerInterface object is provided to the script as context, + ** this method gives a clean interface for the script to log a line. + *******************************************************************************/ + default void log(String message) + { + acceptLogLine(message); + } + + /******************************************************************************* + ** Called if the script fails with an exception. + *******************************************************************************/ + void acceptException(Exception exception); + + /******************************************************************************* + ** Called if the script completes without exception. + *******************************************************************************/ + void acceptExecutionEnd(Serializable output); + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/logging/StoreScriptLogAndScriptLogLineExecutionLogger.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/logging/StoreScriptLogAndScriptLogLineExecutionLogger.java new file mode 100644 index 00000000..6e0b84fc --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/logging/StoreScriptLogAndScriptLogLineExecutionLogger.java @@ -0,0 +1,136 @@ +/* + * 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.actions.scripts.logging; + + +import java.io.Serializable; +import java.util.List; +import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; +import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction; +import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeInput; +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.utils.CollectionUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + + +/******************************************************************************* + ** Implementation of a code execution logger that logs into scriptLog and scriptLogLine + ** tables - e.g., as defined in ScriptMetaDataProvider. + *******************************************************************************/ +public class StoreScriptLogAndScriptLogLineExecutionLogger extends BuildScriptLogAndScriptLogLineExecutionLogger +{ + private static final Logger LOG = LogManager.getLogger(StoreScriptLogAndScriptLogLineExecutionLogger.class); + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public StoreScriptLogAndScriptLogLineExecutionLogger(Serializable scriptId, Serializable scriptRevisionId) + { + super(scriptId, scriptRevisionId); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void acceptExecutionStart(ExecuteCodeInput executeCodeInput) + { + try + { + super.acceptExecutionStart(executeCodeInput); + + InsertInput insertInput = new InsertInput(executeCodeInput.getInstance()); + insertInput.setSession(executeCodeInput.getSession()); + insertInput.setTableName("scriptLog"); + insertInput.setRecords(List.of(getScriptLog())); + InsertOutput insertOutput = new InsertAction().execute(insertInput); + + setScriptLog(insertOutput.getRecords().get(0)); + } + catch(Exception e) + { + LOG.warn("Error starting storage of script log", e); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void acceptException(Exception exception) + { + store(null, exception); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void acceptExecutionEnd(Serializable output) + { + store(output, null); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private void store(Serializable output, Exception exception) + { + try + { + updateHeaderAtEnd(output, exception); + UpdateInput updateInput = new UpdateInput(executeCodeInput.getInstance()); + updateInput.setSession(executeCodeInput.getSession()); + updateInput.setTableName("scriptLog"); + updateInput.setRecords(List.of(getScriptLog())); + new UpdateAction().execute(updateInput); + + if(CollectionUtils.nullSafeHasContents(getScriptLogLines())) + { + InsertInput insertInput = new InsertInput(executeCodeInput.getInstance()); + insertInput.setSession(executeCodeInput.getSession()); + insertInput.setTableName("scriptLogLine"); + insertInput.setRecords(getScriptLogLines()); + new InsertAction().execute(insertInput); + } + } + catch(Exception e) + { + LOG.warn("Error storing script log", e); + } + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/GetAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/GetAction.java new file mode 100644 index 00000000..da923b7b --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/GetAction.java @@ -0,0 +1,161 @@ +/* + * 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.actions.tables; + + +import java.util.List; +import java.util.Optional; +import java.util.function.Function; +import com.kingsrook.qqq.backend.core.actions.ActionHelper; +import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader; +import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers; +import com.kingsrook.qqq.backend.core.actions.interfaces.GetInterface; +import com.kingsrook.qqq.backend.core.actions.values.QPossibleValueTranslator; +import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter; +import com.kingsrook.qqq.backend.core.exceptions.QException; +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.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.data.QRecord; +import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher; +import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface; + + +/******************************************************************************* + ** Action to run a get against a table. + ** + *******************************************************************************/ +public class GetAction +{ + private Optional> postGetRecordCustomizer; + + private GetInput getInput; + private QValueFormatter qValueFormatter; + private QPossibleValueTranslator qPossibleValueTranslator; + + + + /******************************************************************************* + ** + *******************************************************************************/ + public GetOutput execute(GetInput getInput) throws QException + { + ActionHelper.validateSession(getInput); + + postGetRecordCustomizer = QCodeLoader.getTableCustomizerFunction(getInput.getTable(), TableCustomizers.POST_QUERY_RECORD.getRole()); + this.getInput = getInput; + + QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher(); + QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(getInput.getBackend()); + // todo pre-customization - just get to modify the request? + + GetInterface getInterface = null; + try + { + getInterface = qModule.getGetInterface(); + } + catch(IllegalStateException ise) + { + //////////////////////////////////////////////////////////////////////////////////////////////// + // if a module doesn't implement Get directly - try to do a Get by a Query by the primary key // + // see below. // + //////////////////////////////////////////////////////////////////////////////////////////////// + } + + GetOutput getOutput; + if(getInterface != null) + { + getOutput = getInterface.execute(getInput); + } + else + { + getOutput = performGetViaQuery(getInput); + } + + if(getOutput.getRecord() != null) + { + getOutput.setRecord(postRecordActions(getOutput.getRecord())); + } + + return getOutput; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private GetOutput performGetViaQuery(GetInput getInput) throws QException + { + QueryInput queryInput = new QueryInput(getInput.getInstance()); + queryInput.setSession(getInput.getSession()); + queryInput.setTableName(getInput.getTableName()); + queryInput.setFilter(new QQueryFilter().withCriteria(new QFilterCriteria(getInput.getTable().getPrimaryKeyField(), QCriteriaOperator.EQUALS, List.of(getInput.getPrimaryKey())))); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + + GetOutput getOutput = new GetOutput(); + if(!queryOutput.getRecords().isEmpty()) + { + getOutput.setRecord(queryOutput.getRecords().get(0)); + } + return (getOutput); + } + + + + /******************************************************************************* + ** Run the necessary actions on a record. This may include setting display values, + ** translating possible values, and running post-record customizations. + *******************************************************************************/ + public QRecord postRecordActions(QRecord record) + { + QRecord returnRecord = record; + if(this.postGetRecordCustomizer.isPresent()) + { + returnRecord = postGetRecordCustomizer.get().apply(record); + } + + if(getInput.getShouldTranslatePossibleValues()) + { + if(qPossibleValueTranslator == null) + { + qPossibleValueTranslator = new QPossibleValueTranslator(getInput.getInstance(), getInput.getSession()); + } + qPossibleValueTranslator.translatePossibleValuesInRecords(getInput.getTable(), List.of(returnRecord)); + } + + if(getInput.getShouldGenerateDisplayValues()) + { + if(qValueFormatter == null) + { + qValueFormatter = new QValueFormatter(); + } + qValueFormatter.setDisplayValuesInRecords(getInput.getTable(), List.of(returnRecord)); + } + + return (returnRecord); + } +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QPossibleValueTranslator.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QPossibleValueTranslator.java index 31ccea38..31c3f315 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QPossibleValueTranslator.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QPossibleValueTranslator.java @@ -406,8 +406,11 @@ public class QPossibleValueTranslator ///////////////////////////////////////////////////////////////////////////////////////// // this is needed to get record labels, which are what we use here... unclear if best! // ///////////////////////////////////////////////////////////////////////////////////////// - queryInput.setShouldTranslatePossibleValues(true); - queryInput.setShouldGenerateDisplayValues(true); + if(notTooDeep()) + { + queryInput.setShouldTranslatePossibleValues(true); + queryInput.setShouldGenerateDisplayValues(true); + } QueryOutput queryOutput = new QueryAction().execute(queryInput); @@ -428,4 +431,24 @@ public class QPossibleValueTranslator } } + + + /******************************************************************************* + ** Avoid infinite recursion, for where one field's PVS depends on another's... + ** not too smart, just breaks at 5... + *******************************************************************************/ + private boolean notTooDeep() + { + int count = 0; + for(StackTraceElement stackTraceElement : new Throwable().getStackTrace()) + { + if(stackTraceElement.getMethodName().equals("translatePossibleValuesInRecords")) + { + count++; + } + } + + return (count < 5); + } + } 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 791986a2..e6772489 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 @@ -184,6 +184,7 @@ public class QValueFormatter } catch(Exception e) { + LOG.debug("Error formatting record label", e); return (formatRecordLabelExceptionalCases(table, record)); } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/exceptions/QCodeException.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/exceptions/QCodeException.java new file mode 100644 index 00000000..12e16fb5 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/exceptions/QCodeException.java @@ -0,0 +1,79 @@ +/* + * 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.exceptions; + + +/******************************************************************************* + ** Exception thrown while executing custom code in QQQ. + ** + ** Context field is meant to give the user "context" for where the error occurred + ** - e.g., a line number or word that was bad. + *******************************************************************************/ +public class QCodeException extends QException +{ + private String context; + + + + /******************************************************************************* + ** Constructor of message + ** + *******************************************************************************/ + public QCodeException(String message) + { + super(message); + } + + + + /******************************************************************************* + ** Constructor of message & cause + ** + *******************************************************************************/ + public QCodeException(String message, Throwable cause) + { + super(message, cause); + } + + + + /******************************************************************************* + ** Getter for context + ** + *******************************************************************************/ + public String getContext() + { + return context; + } + + + + /******************************************************************************* + ** Setter for context + ** + *******************************************************************************/ + public void setContext(String context) + { + this.context = context; + } + +} 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 573b8ba2..078c7843 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 @@ -316,6 +316,24 @@ public class QInstanceEnricher { generateAppSections(app); } + + for(QAppSection section : CollectionUtils.nonNullList(app.getSections())) + { + enrichAppSection(section); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private void enrichAppSection(QAppSection section) + { + if(!StringUtils.hasContent(section.getLabel())) + { + section.setLabel(nameToLabel(section.getName())); + } } @@ -590,7 +608,7 @@ public class QInstanceEnricher **
  • TLAAndAnotherTLA -> tla_and_another_tla
  • ** *******************************************************************************/ - static String inferBackendName(String fieldName) + public static String inferBackendName(String fieldName) { //////////////////////////////////////////////////////////////////////////////////////// // build a list of words in the name, then join them with _ and lower-case the result // 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 6f344f10..88bf8272 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 @@ -31,13 +31,16 @@ import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.function.Consumer; +import java.util.function.Supplier; import java.util.stream.Stream; import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationHandler; import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers; import com.kingsrook.qqq.backend.core.actions.processes.BackendStep; import com.kingsrook.qqq.backend.core.actions.values.QCustomPossibleValueProvider; import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException; +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.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType; @@ -50,6 +53,8 @@ import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaD 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.queues.SQSQueueProviderMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportDataSource; +import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportView; import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier; @@ -118,6 +123,7 @@ public class QInstanceValidator validateAutomationProviders(qInstance); validateTables(qInstance); validateProcesses(qInstance); + validateReports(qInstance); validateApps(qInstance); validatePossibleValueSources(qInstance); validateQueuesAndProviders(qInstance); @@ -186,6 +192,8 @@ public class QInstanceValidator qInstance.getBackends().forEach((backendName, backend) -> { assertCondition(Objects.equals(backendName, backend.getName()), "Inconsistent naming for backend: " + backendName + "/" + backend.getName() + "."); + + backend.performValidation(this); }); } } @@ -258,6 +266,8 @@ public class QInstanceValidator ////////////////////////////////////////// Set fieldNamesInSections = new HashSet<>(); QFieldSection tier1Section = null; + Set usedSectionNames = new HashSet<>(); + Set usedSectionLabels = new HashSet<>(); if(table.getSections() != null) { for(QFieldSection section : table.getSections()) @@ -268,6 +278,12 @@ public class QInstanceValidator assertCondition(tier1Section == null, "Table " + tableName + " has more than 1 section listed as Tier 1"); tier1Section = section; } + + assertCondition(!usedSectionNames.contains(section.getName()), "Table " + tableName + " has more than 1 section named " + section.getName()); + usedSectionNames.add(section.getName()); + + assertCondition(!usedSectionLabels.contains(section.getLabel()), "Table " + tableName + " has more than 1 section labeled " + section.getLabel()); + usedSectionLabels.add(section.getLabel()); } } @@ -716,6 +732,133 @@ public class QInstanceValidator + /******************************************************************************* + ** + *******************************************************************************/ + private void validateReports(QInstance qInstance) + { + if(CollectionUtils.nullSafeHasContents(qInstance.getReports())) + { + qInstance.getReports().forEach((reportName, report) -> + { + assertCondition(Objects.equals(reportName, report.getName()), "Inconsistent naming for report: " + reportName + "/" + report.getName() + "."); + validateAppChildHasValidParentAppName(qInstance, report); + + //////////////////////////////////////// + // validate dataSources in the report // + //////////////////////////////////////// + Set usedDataSourceNames = new HashSet<>(); + if(assertCondition(CollectionUtils.nullSafeHasContents(report.getDataSources()), "At least 1 data source must be defined in report " + reportName + ".")) + { + int index = 0; + for(QReportDataSource dataSource : report.getDataSources()) + { + assertCondition(StringUtils.hasContent(dataSource.getName()), "Missing name for a dataSource at index " + index + " in report " + reportName); + index++; + + assertCondition(!usedDataSourceNames.contains(dataSource.getName()), "More than one dataSource with name " + dataSource.getName() + " in report " + reportName); + usedDataSourceNames.add(dataSource.getName()); + + String dataSourceErrorPrefix = "Report " + reportName + " data source " + dataSource.getName() + " "; + + if(StringUtils.hasContent(dataSource.getSourceTable())) + { + assertCondition(dataSource.getStaticDataSupplier() == null, dataSourceErrorPrefix + "has both a sourceTable and a staticDataSupplier (exactly 1 is required)."); + if(assertCondition(qInstance.getTable(dataSource.getSourceTable()) != null, dataSourceErrorPrefix + "source table " + dataSource.getSourceTable() + " is not a table in this instance.")) + { + if(dataSource.getQueryFilter() != null) + { + validateQueryFilter("In " + dataSourceErrorPrefix + "query filter - ", qInstance.getTable(dataSource.getSourceTable()), dataSource.getQueryFilter()); + } + } + } + else if(dataSource.getStaticDataSupplier() != null) + { + validateSimpleCodeReference(dataSourceErrorPrefix, dataSource.getStaticDataSupplier(), Supplier.class); + } + else + { + errors.add(dataSourceErrorPrefix + "does not have a sourceTable or a staticDataSupplier (exactly 1 is required)."); + } + } + } + + //////////////////////////////////////// + // validate dataSources in the report // + //////////////////////////////////////// + if(assertCondition(CollectionUtils.nullSafeHasContents(report.getViews()), "At least 1 view must be defined in report " + reportName + ".")) + { + int index = 0; + Set usedViewNames = new HashSet<>(); + for(QReportView view : report.getViews()) + { + assertCondition(StringUtils.hasContent(view.getName()), "Missing name for a view at index " + index + " in report " + reportName); + index++; + + assertCondition(!usedViewNames.contains(view.getName()), "More than one view with name " + view.getName() + " in report " + reportName); + usedViewNames.add(view.getName()); + + String viewErrorPrefix = "Report " + reportName + " view " + view.getName() + " "; + assertCondition(view.getType() != null, viewErrorPrefix + " is missing its type."); + if(assertCondition(StringUtils.hasContent(view.getDataSourceName()), viewErrorPrefix + " is missing a dataSourceName")) + { + assertCondition(usedDataSourceNames.contains(view.getDataSourceName()), viewErrorPrefix + " has an unrecognized dataSourceName: " + view.getDataSourceName()); + } + + if(StringUtils.hasContent(view.getVarianceDataSourceName())) + { + assertCondition(usedDataSourceNames.contains(view.getVarianceDataSourceName()), viewErrorPrefix + " has an unrecognized varianceDataSourceName: " + view.getVarianceDataSourceName()); + } + + // actually, this is okay if there's a customizer, so... + assertCondition(CollectionUtils.nullSafeHasContents(view.getColumns()), viewErrorPrefix + " does not have any columns."); + + // todo - all these too... + // view.getPivotFields(); + // view.getViewCustomizer(); // validate code ref + // view.getRecordTransformStep(); // validate code ref + // view.getOrderByFields(); // make sure valid field names? + // view.getIncludePivotSubTotals(); // only for pivot type + // view.getTitleFormat(); view.getTitleFields(); // validate these match? + } + } + }); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private void validateQueryFilter(String context, QTableMetaData table, QQueryFilter queryFilter) + { + for(QFilterCriteria criterion : CollectionUtils.nonNullList(queryFilter.getCriteria())) + { + if(assertCondition(StringUtils.hasContent(criterion.getFieldName()), context + "Missing fieldName for a criteria")) + { + assertNoException(() -> table.getField(criterion.getFieldName()), context + "Criteria fieldName " + criterion.getFieldName() + " is not a field in this table."); + } + assertCondition(criterion.getOperator() != null, context + "Missing operator for a criteria on fieldName " + criterion.getFieldName()); + assertCondition(criterion.getValues() != null, context + "Missing values for a criteria on fieldName " + criterion.getFieldName()); // todo - what about ops w/ no value (BLANK) + } + + for(QFilterOrderBy orderBy : CollectionUtils.nonNullList(queryFilter.getOrderBys())) + { + if(assertCondition(StringUtils.hasContent(orderBy.getFieldName()), context + "Missing fieldName for an orderBy")) + { + assertNoException(() -> table.getField(orderBy.getFieldName()), context + "OrderBy fieldName " + orderBy.getFieldName() + " is not a field in this table."); + } + } + + for(QQueryFilter subFilter : CollectionUtils.nonNullList(queryFilter.getSubFilters())) + { + validateQueryFilter(context, table, subFilter); + } + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -978,7 +1121,7 @@ public class QInstanceValidator ** But if it's false, 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 assertCondition(boolean condition, String message) + public boolean assertCondition(boolean condition, String message) { if(!condition) { @@ -1035,4 +1178,15 @@ public class QInstanceValidator LOG.info("Validation warning: " + message); } } + + + + /******************************************************************************* + ** Getter for errors + ** + *******************************************************************************/ + public List getErrors() + { + return errors; + } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunBackendStepInput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunBackendStepInput.java index 69cf5787..0ffbd612 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunBackendStepInput.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunBackendStepInput.java @@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.model.actions.processes; import java.io.Serializable; +import java.time.Instant; import java.time.LocalDate; import java.util.List; import java.util.Map; @@ -44,12 +45,14 @@ import com.kingsrook.qqq.backend.core.utils.ValueUtils; *******************************************************************************/ public class RunBackendStepInput extends AbstractActionInput { - private ProcessState processState; - private String processName; - private String tableName; - private String stepName; - private QProcessCallback callback; - private AsyncJobCallback asyncJobCallback; + private ProcessState processState; + private String processName; + private String tableName; + private String stepName; + private QProcessCallback callback; + private AsyncJobCallback asyncJobCallback; + private RunProcessInput.FrontendStepBehavior frontendStepBehavior; + private Instant basepullLastRunTime; //////////////////////////////////////////////////////////////////////////// // note - new fields should generally be added in method: cloneFieldsInto // @@ -416,6 +419,17 @@ public class RunBackendStepInput extends AbstractActionInput + /******************************************************************************* + ** Getter for a single field's value + ** + *******************************************************************************/ + public Instant getValueInstant(String fieldName) + { + return (ValueUtils.getValueAsInstant(getValue(fieldName))); + } + + + /******************************************************************************* ** Accessor for processState - protected, because we generally want to access ** its members through wrapper methods, we think @@ -453,4 +467,72 @@ public class RunBackendStepInput extends AbstractActionInput return (asyncJobCallback); } + + + /******************************************************************************* + ** Getter for frontendStepBehavior + ** + *******************************************************************************/ + public RunProcessInput.FrontendStepBehavior getFrontendStepBehavior() + { + return frontendStepBehavior; + } + + + + /******************************************************************************* + ** Setter for frontendStepBehavior + ** + *******************************************************************************/ + public void setFrontendStepBehavior(RunProcessInput.FrontendStepBehavior frontendStepBehavior) + { + this.frontendStepBehavior = frontendStepBehavior; + } + + + + /******************************************************************************* + ** Fluent setter for frontendStepBehavior + ** + *******************************************************************************/ + public RunBackendStepInput withFrontendStepBehavior(RunProcessInput.FrontendStepBehavior frontendStepBehavior) + { + this.frontendStepBehavior = frontendStepBehavior; + return (this); + } + + + + /******************************************************************************* + ** Getter for basepullLastRunTime + ** + *******************************************************************************/ + public Instant getBasepullLastRunTime() + { + return basepullLastRunTime; + } + + + + /******************************************************************************* + ** Setter for basepullLastRunTime + ** + *******************************************************************************/ + public void setBasepullLastRunTime(Instant basepullLastRunTime) + { + this.basepullLastRunTime = basepullLastRunTime; + } + + + + /******************************************************************************* + ** Fluent setter for basepullLastRunTime + ** + *******************************************************************************/ + public RunBackendStepInput withBasepullLastRunTime(Instant basepullLastRunTime) + { + this.basepullLastRunTime = basepullLastRunTime; + return (this); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/scripts/ExecuteCodeInput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/scripts/ExecuteCodeInput.java new file mode 100644 index 00000000..6bb4df7d --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/scripts/ExecuteCodeInput.java @@ -0,0 +1,223 @@ +/* + * 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.actions.scripts; + + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; +import com.kingsrook.qqq.backend.core.actions.scripts.logging.QCodeExecutionLoggerInterface; +import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class ExecuteCodeInput extends AbstractActionInput +{ + private QCodeReference codeReference; + private Map input; + private Map context; + private QCodeExecutionLoggerInterface executionLogger; + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public ExecuteCodeInput(QInstance qInstance) + { + super(qInstance); + } + + + + /******************************************************************************* + ** Getter for codeReference + ** + *******************************************************************************/ + public QCodeReference getCodeReference() + { + return codeReference; + } + + + + /******************************************************************************* + ** Setter for codeReference + ** + *******************************************************************************/ + public void setCodeReference(QCodeReference codeReference) + { + this.codeReference = codeReference; + } + + + + /******************************************************************************* + ** Fluent setter for codeReference + ** + *******************************************************************************/ + public ExecuteCodeInput withCodeReference(QCodeReference codeReference) + { + this.codeReference = codeReference; + return (this); + } + + + + /******************************************************************************* + ** Getter for input + ** + *******************************************************************************/ + public Map getInput() + { + return input; + } + + + + /******************************************************************************* + ** Setter for input + ** + *******************************************************************************/ + public void setInput(Map input) + { + this.input = input; + } + + + + /******************************************************************************* + ** Fluent setter for input + ** + *******************************************************************************/ + public ExecuteCodeInput withInput(Map input) + { + this.input = input; + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for input + ** + *******************************************************************************/ + public ExecuteCodeInput withInput(String key, Serializable value) + { + if(this.input == null) + { + input = new HashMap<>(); + } + this.input.put(key, value); + return (this); + } + + + + /******************************************************************************* + ** Getter for context + ** + *******************************************************************************/ + public Map getContext() + { + return context; + } + + + + /******************************************************************************* + ** Setter for context + ** + *******************************************************************************/ + public void setContext(Map context) + { + this.context = context; + } + + + + /******************************************************************************* + ** Fluent setter for context + ** + *******************************************************************************/ + public ExecuteCodeInput withContext(Map context) + { + this.context = context; + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for context + ** + *******************************************************************************/ + public ExecuteCodeInput withContext(String key, Serializable value) + { + if(this.context == null) + { + context = new HashMap<>(); + } + this.context.put(key, value); + return (this); + } + + + + /******************************************************************************* + ** Getter for executionLogger + ** + *******************************************************************************/ + public QCodeExecutionLoggerInterface getExecutionLogger() + { + return executionLogger; + } + + + + /******************************************************************************* + ** Setter for executionLogger + ** + *******************************************************************************/ + public void setExecutionLogger(QCodeExecutionLoggerInterface executionLogger) + { + this.executionLogger = executionLogger; + } + + + + /******************************************************************************* + ** Fluent setter for executionLogger + ** + *******************************************************************************/ + public ExecuteCodeInput withExecutionLogger(QCodeExecutionLoggerInterface executionLogger) + { + this.executionLogger = executionLogger; + return (this); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/scripts/ExecuteCodeOutput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/scripts/ExecuteCodeOutput.java new file mode 100644 index 00000000..bb66522f --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/scripts/ExecuteCodeOutput.java @@ -0,0 +1,69 @@ +/* + * 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.actions.scripts; + + +import java.io.Serializable; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class ExecuteCodeOutput +{ + private Serializable output; + + + + /******************************************************************************* + ** Getter for output + ** + *******************************************************************************/ + public Serializable getOutput() + { + return output; + } + + + + /******************************************************************************* + ** Setter for output + ** + *******************************************************************************/ + public void setOutput(Serializable output) + { + this.output = output; + } + + + + /******************************************************************************* + ** Fluent setter for output + ** + *******************************************************************************/ + public ExecuteCodeOutput withOutput(Serializable output) + { + this.output = output; + return (this); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/scripts/RunAssociatedScriptInput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/scripts/RunAssociatedScriptInput.java new file mode 100644 index 00000000..77acfe43 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/scripts/RunAssociatedScriptInput.java @@ -0,0 +1,154 @@ +/* + * 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.actions.scripts; + + +import java.io.Serializable; +import java.util.Map; +import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.code.AssociatedScriptCodeReference; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class RunAssociatedScriptInput extends AbstractTableActionInput +{ + private AssociatedScriptCodeReference codeReference; + private Map inputValues; + + private Serializable outputObject; + + + + /******************************************************************************* + ** + *******************************************************************************/ + public RunAssociatedScriptInput(QInstance qInstance) + { + super(qInstance); + } + + + + /******************************************************************************* + ** Getter for codeReference + ** + *******************************************************************************/ + public AssociatedScriptCodeReference getCodeReference() + { + return codeReference; + } + + + + /******************************************************************************* + ** Setter for codeReference + ** + *******************************************************************************/ + public void setCodeReference(AssociatedScriptCodeReference codeReference) + { + this.codeReference = codeReference; + } + + + + /******************************************************************************* + ** Fluent setter for codeReference + ** + *******************************************************************************/ + public RunAssociatedScriptInput withCodeReference(AssociatedScriptCodeReference codeReference) + { + this.codeReference = codeReference; + return (this); + } + + + + /******************************************************************************* + ** Getter for inputValues + ** + *******************************************************************************/ + public Map getInputValues() + { + return inputValues; + } + + + + /******************************************************************************* + ** Setter for inputValues + ** + *******************************************************************************/ + public void setInputValues(Map inputValues) + { + this.inputValues = inputValues; + } + + + + /******************************************************************************* + ** Fluent setter for inputValues + ** + *******************************************************************************/ + public RunAssociatedScriptInput withInputValues(Map inputValues) + { + this.inputValues = inputValues; + return (this); + } + + + + /******************************************************************************* + ** Getter for outputObject + ** + *******************************************************************************/ + public Serializable getOutputObject() + { + return outputObject; + } + + + + /******************************************************************************* + ** Setter for outputObject + ** + *******************************************************************************/ + public void setOutputObject(Serializable outputObject) + { + this.outputObject = outputObject; + } + + + + /******************************************************************************* + ** Fluent setter for outputObject + ** + *******************************************************************************/ + public RunAssociatedScriptInput withOutputObject(Serializable outputObject) + { + this.outputObject = outputObject; + return (this); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/scripts/RunAssociatedScriptOutput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/scripts/RunAssociatedScriptOutput.java new file mode 100644 index 00000000..1b80a54f --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/scripts/RunAssociatedScriptOutput.java @@ -0,0 +1,70 @@ +/* + * 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.actions.scripts; + + +import java.io.Serializable; +import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class RunAssociatedScriptOutput extends AbstractActionOutput +{ + private Serializable output; + + + + /******************************************************************************* + ** Getter for output + ** + *******************************************************************************/ + public Serializable getOutput() + { + return output; + } + + + + /******************************************************************************* + ** Setter for output + ** + *******************************************************************************/ + public void setOutput(Serializable output) + { + this.output = output; + } + + + + /******************************************************************************* + ** Fluent setter for output + ** + *******************************************************************************/ + public RunAssociatedScriptOutput withOutput(Serializable output) + { + this.output = output; + return (this); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/scripts/StoreAssociatedScriptInput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/scripts/StoreAssociatedScriptInput.java new file mode 100644 index 00000000..4f3915b4 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/scripts/StoreAssociatedScriptInput.java @@ -0,0 +1,188 @@ +/* + * 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.actions.scripts; + + +import java.io.Serializable; +import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class StoreAssociatedScriptInput extends AbstractTableActionInput +{ + private String fieldName; + private Serializable recordPrimaryKey; + + private String code; + private String commitMessage; + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public StoreAssociatedScriptInput(QInstance instance) + { + super(instance); + } + + + + /******************************************************************************* + ** Getter for fieldName + ** + *******************************************************************************/ + public String getFieldName() + { + return fieldName; + } + + + + /******************************************************************************* + ** Setter for fieldName + ** + *******************************************************************************/ + public void setFieldName(String fieldName) + { + this.fieldName = fieldName; + } + + + + /******************************************************************************* + ** Fluent setter for fieldName + ** + *******************************************************************************/ + public StoreAssociatedScriptInput withFieldName(String fieldName) + { + this.fieldName = fieldName; + return (this); + } + + + + /******************************************************************************* + ** Getter for recordPrimaryKey + ** + *******************************************************************************/ + public Serializable getRecordPrimaryKey() + { + return recordPrimaryKey; + } + + + + /******************************************************************************* + ** Setter for recordPrimaryKey + ** + *******************************************************************************/ + public void setRecordPrimaryKey(Serializable recordPrimaryKey) + { + this.recordPrimaryKey = recordPrimaryKey; + } + + + + /******************************************************************************* + ** Fluent setter for recordPrimaryKey + ** + *******************************************************************************/ + public StoreAssociatedScriptInput withRecordPrimaryKey(Serializable recordPrimaryKey) + { + this.recordPrimaryKey = recordPrimaryKey; + return (this); + } + + + + /******************************************************************************* + ** Getter for code + ** + *******************************************************************************/ + public String getCode() + { + return code; + } + + + + /******************************************************************************* + ** Setter for code + ** + *******************************************************************************/ + public void setCode(String code) + { + this.code = code; + } + + + + /******************************************************************************* + ** Fluent setter for code + ** + *******************************************************************************/ + public StoreAssociatedScriptInput withCode(String code) + { + this.code = code; + return (this); + } + + + + /******************************************************************************* + ** Getter for commitMessage + ** + *******************************************************************************/ + public String getCommitMessage() + { + return commitMessage; + } + + + + /******************************************************************************* + ** Setter for commitMessage + ** + *******************************************************************************/ + public void setCommitMessage(String commitMessage) + { + this.commitMessage = commitMessage; + } + + + + /******************************************************************************* + ** Fluent setter for commitMessage + ** + *******************************************************************************/ + public StoreAssociatedScriptInput withCommitMessage(String commitMessage) + { + this.commitMessage = commitMessage; + return (this); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/scripts/StoreAssociatedScriptOutput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/scripts/StoreAssociatedScriptOutput.java new file mode 100644 index 00000000..12549e0d --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/scripts/StoreAssociatedScriptOutput.java @@ -0,0 +1,33 @@ +/* + * 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.actions.scripts; + + +import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class StoreAssociatedScriptOutput extends AbstractActionOutput +{ +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/scripts/TestScriptInput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/scripts/TestScriptInput.java new file mode 100644 index 00000000..33871f38 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/scripts/TestScriptInput.java @@ -0,0 +1,187 @@ +/* + * 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.actions.scripts; + + +import java.io.Serializable; +import java.util.Map; +import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class TestScriptInput extends AbstractTableActionInput +{ + private Serializable recordPrimaryKey; + private String code; + private Serializable scriptTypeId; + private Map inputValues; + + + + /******************************************************************************* + ** + *******************************************************************************/ + public TestScriptInput(QInstance qInstance) + { + super(qInstance); + } + + + + /******************************************************************************* + ** Getter for recordPrimaryKey + ** + *******************************************************************************/ + public Serializable getRecordPrimaryKey() + { + return recordPrimaryKey; + } + + + + /******************************************************************************* + ** Setter for recordPrimaryKey + ** + *******************************************************************************/ + public void setRecordPrimaryKey(Serializable recordPrimaryKey) + { + this.recordPrimaryKey = recordPrimaryKey; + } + + + + /******************************************************************************* + ** Fluent setter for recordPrimaryKey + ** + *******************************************************************************/ + public TestScriptInput withRecordPrimaryKey(Serializable recordPrimaryKey) + { + this.recordPrimaryKey = recordPrimaryKey; + return (this); + } + + + + /******************************************************************************* + ** Getter for inputValues + ** + *******************************************************************************/ + public Map getInputValues() + { + return inputValues; + } + + + + /******************************************************************************* + ** Setter for inputValues + ** + *******************************************************************************/ + public void setInputValues(Map inputValues) + { + this.inputValues = inputValues; + } + + + + /******************************************************************************* + ** Fluent setter for inputValues + ** + *******************************************************************************/ + public TestScriptInput withInputValues(Map inputValues) + { + this.inputValues = inputValues; + return (this); + } + + + + /******************************************************************************* + ** Getter for code + ** + *******************************************************************************/ + public String getCode() + { + return code; + } + + + + /******************************************************************************* + ** Setter for code + ** + *******************************************************************************/ + public void setCode(String code) + { + this.code = code; + } + + + + /******************************************************************************* + ** Fluent setter for code + ** + *******************************************************************************/ + public TestScriptInput withCode(String code) + { + this.code = code; + return (this); + } + + + + /******************************************************************************* + ** Getter for scriptTypeId + ** + *******************************************************************************/ + public Serializable getScriptTypeId() + { + return scriptTypeId; + } + + + + /******************************************************************************* + ** Setter for scriptTypeId + ** + *******************************************************************************/ + public void setScriptTypeId(Serializable scriptTypeId) + { + this.scriptTypeId = scriptTypeId; + } + + + + /******************************************************************************* + ** Fluent setter for scriptTypeId + ** + *******************************************************************************/ + public TestScriptInput withScriptTypeId(Serializable scriptTypeId) + { + this.scriptTypeId = scriptTypeId; + return (this); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/scripts/TestScriptOutput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/scripts/TestScriptOutput.java new file mode 100644 index 00000000..d6da9222 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/scripts/TestScriptOutput.java @@ -0,0 +1,33 @@ +/* + * 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.actions.scripts; + + +import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class TestScriptOutput extends AbstractActionOutput +{ +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/get/GetInput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/get/GetInput.java new file mode 100644 index 00000000..e08e3f22 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/get/GetInput.java @@ -0,0 +1,186 @@ +/* + * 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.actions.tables.get; + + +import java.io.Serializable; +import com.kingsrook.qqq.backend.core.actions.QBackendTransaction; +import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.session.QSession; + + +/******************************************************************************* + ** Input data for the Get action + ** + *******************************************************************************/ +public class GetInput extends AbstractTableActionInput +{ + private QBackendTransaction transaction; + private Serializable primaryKey; + + private boolean shouldTranslatePossibleValues = false; + private boolean shouldGenerateDisplayValues = false; + + + + /******************************************************************************* + ** + *******************************************************************************/ + public GetInput() + { + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public GetInput(QInstance instance) + { + super(instance); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public GetInput(QInstance instance, QSession session) + { + super(instance); + setSession(session); + } + + + + /******************************************************************************* + ** Getter for primaryKey + ** + *******************************************************************************/ + public Serializable getPrimaryKey() + { + return primaryKey; + } + + + + /******************************************************************************* + ** Setter for primaryKey + ** + *******************************************************************************/ + public void setPrimaryKey(Serializable primaryKey) + { + this.primaryKey = primaryKey; + } + + + + /******************************************************************************* + ** Fluent setter for primaryKey + ** + *******************************************************************************/ + public GetInput withPrimaryKey(Serializable primaryKey) + { + this.primaryKey = primaryKey; + return (this); + } + + + + /******************************************************************************* + ** Getter for shouldTranslatePossibleValues + ** + *******************************************************************************/ + public boolean getShouldTranslatePossibleValues() + { + return shouldTranslatePossibleValues; + } + + + + /******************************************************************************* + ** Setter for shouldTranslatePossibleValues + ** + *******************************************************************************/ + public void setShouldTranslatePossibleValues(boolean shouldTranslatePossibleValues) + { + this.shouldTranslatePossibleValues = shouldTranslatePossibleValues; + } + + + + /******************************************************************************* + ** Getter for shouldGenerateDisplayValues + ** + *******************************************************************************/ + public boolean getShouldGenerateDisplayValues() + { + return shouldGenerateDisplayValues; + } + + + + /******************************************************************************* + ** Setter for shouldGenerateDisplayValues + ** + *******************************************************************************/ + public void setShouldGenerateDisplayValues(boolean shouldGenerateDisplayValues) + { + this.shouldGenerateDisplayValues = shouldGenerateDisplayValues; + } + + + + /******************************************************************************* + ** Getter for transaction + ** + *******************************************************************************/ + public QBackendTransaction getTransaction() + { + return transaction; + } + + + + /******************************************************************************* + ** Setter for transaction + ** + *******************************************************************************/ + public void setTransaction(QBackendTransaction transaction) + { + this.transaction = transaction; + } + + + + /******************************************************************************* + ** Fluent setter for transaction + ** + *******************************************************************************/ + public GetInput withTransaction(QBackendTransaction transaction) + { + this.transaction = transaction; + return (this); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/get/GetOutput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/get/GetOutput.java new file mode 100644 index 00000000..2f9cada6 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/get/GetOutput.java @@ -0,0 +1,72 @@ +/* + * 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.actions.tables.get; + + +import java.io.Serializable; +import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput; +import com.kingsrook.qqq.backend.core.model.data.QRecord; + + +/******************************************************************************* + ** Output for a Get action + ** + *******************************************************************************/ +public class GetOutput extends AbstractActionOutput implements Serializable +{ + private QRecord record; + + + + /******************************************************************************* + ** Getter for record + ** + *******************************************************************************/ + public QRecord getRecord() + { + return record; + } + + + + /******************************************************************************* + ** Setter for record + ** + *******************************************************************************/ + public void setRecord(QRecord record) + { + this.record = record; + } + + + + /******************************************************************************* + ** Fluent setter for record + ** + *******************************************************************************/ + public GetOutput withRecord(QRecord record) + { + this.record = record; + return (this); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QFilterCriteria.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QFilterCriteria.java index 51653a6b..f83e959d 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QFilterCriteria.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QFilterCriteria.java @@ -25,6 +25,9 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.query; import java.io.Serializable; import java.util.ArrayList; import java.util.List; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; /******************************************************************************* @@ -33,6 +36,8 @@ import java.util.List; *******************************************************************************/ public class QFilterCriteria implements Serializable, Cloneable { + private static final Logger LOG = LogManager.getLogger(QFilterCriteria.class); + private String fieldName; private QCriteriaOperator operator; private List values; @@ -183,4 +188,46 @@ public class QFilterCriteria implements Serializable, Cloneable this.values = values; return this; } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public String toString() + { + StringBuilder rs = new StringBuilder(fieldName); + try + { + rs.append(" ").append(operator).append(" "); + if(CollectionUtils.nullSafeHasContents(values)) + { + if(values.size() == 1) + { + rs.append(values.get(0)); + } + else + { + int index = 0; + for(Serializable value : values) + { + if(index++ > 9) + { + rs.append("and ").append(values.size() - index).append(" more"); + break; + } + rs.append(value).append(","); + } + } + } + } + catch(Exception e) + { + LOG.warn("Error in toString", e); + rs.append("Error generating toString..."); + } + + return (rs.toString()); + } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QFilterOrderBy.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QFilterOrderBy.java index 4cfa6395..52eca98c 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QFilterOrderBy.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QFilterOrderBy.java @@ -151,4 +151,14 @@ public class QFilterOrderBy implements Serializable, Cloneable return (this); } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public String toString() + { + return (fieldName + " " + (isAscending ? "ASC" : "DESC")); + } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QQueryFilter.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QQueryFilter.java index f39403de..6d1fc376 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QQueryFilter.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QQueryFilter.java @@ -26,6 +26,8 @@ import java.io.Serializable; import java.util.ArrayList; import java.util.List; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; /******************************************************************************* @@ -34,6 +36,8 @@ import com.kingsrook.qqq.backend.core.utils.CollectionUtils; *******************************************************************************/ public class QQueryFilter implements Serializable, Cloneable { + private static final Logger LOG = LogManager.getLogger(QQueryFilter.class); + private List criteria = new ArrayList<>(); private List orderBys = new ArrayList<>(); @@ -301,4 +305,41 @@ public class QQueryFilter implements Serializable, Cloneable subFilters.add(subFilter); } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public String toString() + { + StringBuilder rs = new StringBuilder("("); + try + { + for(QFilterCriteria criterion : CollectionUtils.nonNullList(criteria)) + { + rs.append(criterion).append(" ").append(getBooleanOperator()); + } + + for(QQueryFilter subFilter : CollectionUtils.nonNullList(subFilters)) + { + rs.append(subFilter); + } + rs.append(")"); + + rs.append("OrderBy["); + for(QFilterOrderBy orderBy : orderBys) + { + rs.append(orderBy).append(","); + } + rs.append("]"); + } + catch(Exception e) + { + LOG.warn("Error in toString", e); + rs.append("Error generating toString..."); + } + + return (rs.toString()); + } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/update/UpdateOutput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/update/UpdateOutput.java index 81c39178..5bde561c 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/update/UpdateOutput.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/update/UpdateOutput.java @@ -22,6 +22,7 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.update; +import java.util.ArrayList; import java.util.List; import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput; import com.kingsrook.qqq.backend.core.model.data.QRecord; @@ -54,4 +55,18 @@ public class UpdateOutput extends AbstractActionOutput { this.records = records; } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public void addRecord(QRecord record) + { + if(this.records == null) + { + this.records = new ArrayList<>(); + } + this.records.add(record); + } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecord.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecord.java index 5e3772e1..8a5bb58e 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecord.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecord.java @@ -64,6 +64,8 @@ public class QRecord implements Serializable private Map backendDetails = new LinkedHashMap<>(); private List errors = new ArrayList<>(); + public static final String BACKEND_DETAILS_TYPE_JSON_SOURCE_OBJECT = "jsonSourceObject"; + /******************************************************************************* @@ -455,6 +457,11 @@ public class QRecord implements Serializable *******************************************************************************/ public Serializable getBackendDetail(String key) { + if(!this.backendDetails.containsKey(key)) + { + return (null); + } + return this.backendDetails.get(key); } @@ -466,7 +473,7 @@ public class QRecord implements Serializable *******************************************************************************/ public String getBackendDetailString(String key) { - return (String) this.backendDetails.get(key); + return (String) getBackendDetail(key); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecordEnum.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecordEnum.java new file mode 100644 index 00000000..28b6198c --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecordEnum.java @@ -0,0 +1,175 @@ +/* + * 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.data; + + +import java.io.Serializable; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.math.BigDecimal; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Optional; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.utils.ListingHash; + + +/******************************************************************************* + ** Base class for enums that are interoperable with QRecords. + *******************************************************************************/ +public interface QRecordEnum +{ + ListingHash, QRecordEntityField> fieldMapping = new ListingHash<>(); + + + /******************************************************************************* + ** Convert this entity to a QRecord. + ** + *******************************************************************************/ + default QRecord toQRecord() throws QException + { + try + { + QRecord qRecord = new QRecord(); + + List fieldList = getFieldList(this.getClass()); + for(QRecordEntityField qRecordEntityField : fieldList) + { + qRecord.setValue(qRecordEntityField.getFieldName(), (Serializable) qRecordEntityField.getGetter().invoke(this)); + } + + return (qRecord); + } + catch(Exception e) + { + throw (new QException("Error building qRecord from entity.", e)); + } + } + + + /******************************************************************************* + ** + *******************************************************************************/ + public static List getFieldList(Class c) + { + if(!fieldMapping.containsKey(c)) + { + List fieldList = new ArrayList<>(); + for(Method possibleGetter : c.getMethods()) + { + if(isGetter(possibleGetter)) + { + String fieldName = getFieldNameFromGetter(possibleGetter); + Optional fieldAnnotation = getQFieldAnnotation(c, fieldName); + fieldList.add(new QRecordEntityField(fieldName, possibleGetter, null, possibleGetter.getReturnType(), fieldAnnotation.orElse(null))); + } + } + fieldMapping.put(c, fieldList); + } + return (fieldMapping.get(c)); + } + + + /******************************************************************************* + ** + *******************************************************************************/ + public static Optional getQFieldAnnotation(Class c, String fieldName) + { + try + { + Field field = c.getDeclaredField(fieldName); + return (Optional.ofNullable(field.getAnnotation(QField.class))); + } + catch(NoSuchFieldException e) + { + ////////////////////////////////////////// + // ok, we just won't have an annotation // + ////////////////////////////////////////// + } + return (Optional.empty()); + } + + + /******************************************************************************* + ** + *******************************************************************************/ + public static String getFieldNameFromGetter(Method getter) + { + String nameWithoutGet = getter.getName().replaceFirst("^get", ""); + if(nameWithoutGet.length() == 1) + { + return (nameWithoutGet.toLowerCase(Locale.ROOT)); + } + return (nameWithoutGet.substring(0, 1).toLowerCase(Locale.ROOT) + nameWithoutGet.substring(1)); + } + + + /******************************************************************************* + ** + *******************************************************************************/ + private static boolean isGetter(Method method) + { + if(method.getParameterTypes().length == 0 && method.getName().matches("^get[A-Z].*")) + { + if(isSupportedFieldType(method.getReturnType())) + { + return (true); + } + else + { + if(!method.getName().equals("getClass")) + { + System.err.println("Method [" + method.getName() + "] looks like a getter, but its return type, [" + method.getReturnType() + "], isn't supported."); + } + } + } + return (false); + } + + + /******************************************************************************* + ** + *******************************************************************************/ + private static boolean isSupportedFieldType(Class returnType) + { + // todo - more types!! + return (returnType.equals(String.class) + || returnType.equals(Integer.class) + || returnType.equals(int.class) + || returnType.equals(Boolean.class) + || returnType.equals(boolean.class) + || returnType.equals(BigDecimal.class) + || returnType.equals(Instant.class) + || returnType.equals(LocalDate.class) + || returnType.equals(LocalTime.class)); + ///////////////////////////////////////////// + // note - this list has implications upon: // + // - QFieldType.fromClass // + // - QRecordEntityField.convertValueType // + ///////////////////////////////////////////// + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QBackendMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QBackendMetaData.java index ea4cd840..6c5d6985 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QBackendMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QBackendMetaData.java @@ -22,8 +22,13 @@ package com.kingsrook.qqq.backend.core.model.metadata; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.kingsrook.qqq.backend.core.instances.QInstanceValidator; import com.kingsrook.qqq.backend.core.model.metadata.serialization.QBackendMetaDataDeserializer; +import com.kingsrook.qqq.backend.core.model.metadata.tables.Capability; import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface; @@ -38,6 +43,9 @@ public class QBackendMetaData private String name; private String backendType; + private Set enabledCapabilities = new HashSet<>(); + private Set disabledCapabilities = new HashSet<>(); + // todo - at some point, we may want to apply this to secret properties on subclasses? // @JsonFilter("secretsFilter") @@ -157,4 +165,172 @@ public class QBackendMetaData // noop in base class // //////////////////////// } + + + + /******************************************************************************* + ** Getter for enabledCapabilities + ** + *******************************************************************************/ + public Set getEnabledCapabilities() + { + return enabledCapabilities; + } + + + + /******************************************************************************* + ** Setter for enabledCapabilities + ** + *******************************************************************************/ + public void setEnabledCapabilities(Set enabledCapabilities) + { + this.enabledCapabilities = enabledCapabilities; + } + + + + /******************************************************************************* + ** Fluent setter for enabledCapabilities + ** + *******************************************************************************/ + public QBackendMetaData withEnabledCapabilities(Set enabledCapabilities) + { + this.enabledCapabilities = enabledCapabilities; + return (this); + } + + + + /******************************************************************************* + ** Alternative fluent setter for enabledCapabilities + ** + *******************************************************************************/ + public QBackendMetaData withCapabilities(Set enabledCapabilities) + { + this.enabledCapabilities = enabledCapabilities; + return (this); + } + + + + /******************************************************************************* + ** Alternative fluent setter for a single enabledCapabilities + ** + *******************************************************************************/ + public QBackendMetaData withCapability(Capability capability) + { + if(this.enabledCapabilities == null) + { + this.enabledCapabilities = new HashSet<>(); + } + this.enabledCapabilities.add(capability); + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for enabledCapabilities + ** + *******************************************************************************/ + public QBackendMetaData withCapabilities(Capability... enabledCapabilities) + { + if(this.enabledCapabilities == null) + { + this.enabledCapabilities = new HashSet<>(); + } + this.enabledCapabilities.addAll(Arrays.stream(enabledCapabilities).toList()); + return (this); + } + + + + /******************************************************************************* + ** Getter for disabledCapabilities + ** + *******************************************************************************/ + public Set getDisabledCapabilities() + { + return disabledCapabilities; + } + + + + /******************************************************************************* + ** Setter for disabledCapabilities + ** + *******************************************************************************/ + public void setDisabledCapabilities(Set disabledCapabilities) + { + this.disabledCapabilities = disabledCapabilities; + } + + + + /******************************************************************************* + ** Fluent setter for disabledCapabilities + ** + *******************************************************************************/ + public QBackendMetaData withDisabledCapabilities(Set disabledCapabilities) + { + this.disabledCapabilities = disabledCapabilities; + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for disabledCapabilities + ** + *******************************************************************************/ + public QBackendMetaData withoutCapabilities(Capability... disabledCapabilities) + { + if(this.disabledCapabilities == null) + { + this.disabledCapabilities = new HashSet<>(); + } + this.disabledCapabilities.addAll(Arrays.stream(disabledCapabilities).toList()); + return (this); + } + + + + /******************************************************************************* + ** Alternative fluent setter for disabledCapabilities + ** + *******************************************************************************/ + public QBackendMetaData withoutCapabilities(Set disabledCapabilities) + { + this.disabledCapabilities = disabledCapabilities; + return (this); + } + + + + /******************************************************************************* + ** Alternative fluent setter for a single disabledCapabilities + ** + *******************************************************************************/ + public QBackendMetaData withoutCapability(Capability capability) + { + if(this.disabledCapabilities == null) + { + this.disabledCapabilities = new HashSet<>(); + } + this.disabledCapabilities.add(capability); + return (this); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public void performValidation(QInstanceValidator qInstanceValidator) + { + //////////////////////// + // noop in base class // + //////////////////////// + } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/AssociatedScriptCodeReference.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/AssociatedScriptCodeReference.java new file mode 100644 index 00000000..716b4063 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/AssociatedScriptCodeReference.java @@ -0,0 +1,139 @@ +/* + * 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.metadata.code; + + +import java.io.Serializable; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class AssociatedScriptCodeReference extends QCodeReference +{ + private String recordTable; + private Serializable recordPrimaryKey; + private String fieldName; + + + + /******************************************************************************* + ** Getter for recordTable + ** + *******************************************************************************/ + public String getRecordTable() + { + return recordTable; + } + + + + /******************************************************************************* + ** Setter for recordTable + ** + *******************************************************************************/ + public void setRecordTable(String recordTable) + { + this.recordTable = recordTable; + } + + + + /******************************************************************************* + ** Fluent setter for recordTable + ** + *******************************************************************************/ + public AssociatedScriptCodeReference withRecordTable(String recordTable) + { + this.recordTable = recordTable; + return (this); + } + + + + /******************************************************************************* + ** Getter for recordPrimaryKey + ** + *******************************************************************************/ + public Serializable getRecordPrimaryKey() + { + return recordPrimaryKey; + } + + + + /******************************************************************************* + ** Setter for recordPrimaryKey + ** + *******************************************************************************/ + public void setRecordPrimaryKey(Serializable recordPrimaryKey) + { + this.recordPrimaryKey = recordPrimaryKey; + } + + + + /******************************************************************************* + ** Fluent setter for recordPrimaryKey + ** + *******************************************************************************/ + public AssociatedScriptCodeReference withRecordPrimaryKey(Serializable recordPrimaryKey) + { + this.recordPrimaryKey = recordPrimaryKey; + return (this); + } + + + + /******************************************************************************* + ** Getter for fieldName + ** + *******************************************************************************/ + public String getFieldName() + { + return fieldName; + } + + + + /******************************************************************************* + ** Setter for fieldName + ** + *******************************************************************************/ + public void setFieldName(String fieldName) + { + this.fieldName = fieldName; + } + + + + /******************************************************************************* + ** Fluent setter for fieldName + ** + *******************************************************************************/ + public AssociatedScriptCodeReference withFieldName(String fieldName) + { + this.fieldName = fieldName; + return (this); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/QCodeReference.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/QCodeReference.java index 62f7f7d2..4e7998e5 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/QCodeReference.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/QCodeReference.java @@ -38,6 +38,8 @@ public class QCodeReference implements Serializable private QCodeType codeType; private QCodeUsage codeUsage; + private String inlineCode; + /******************************************************************************* @@ -212,4 +214,38 @@ public class QCodeReference implements Serializable return (this); } + + + /******************************************************************************* + ** Getter for inlineCode + ** + *******************************************************************************/ + public String getInlineCode() + { + return inlineCode; + } + + + + /******************************************************************************* + ** Setter for inlineCode + ** + *******************************************************************************/ + public void setInlineCode(String inlineCode) + { + this.inlineCode = inlineCode; + } + + + + /******************************************************************************* + ** Fluent setter for inlineCode + ** + *******************************************************************************/ + public QCodeReference withInlineCode(String inlineCode) + { + this.inlineCode = inlineCode; + return (this); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/QCodeType.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/QCodeType.java index 6aa9099b..e3f98270 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/QCodeType.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/QCodeType.java @@ -28,5 +28,6 @@ package com.kingsrook.qqq.backend.core.model.metadata.code; *******************************************************************************/ public enum QCodeType { - JAVA + JAVA, + JAVA_SCRIPT } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/QWidgetMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/QWidgetMetaData.java index b3c481a1..45da4911 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/QWidgetMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/QWidgetMetaData.java @@ -22,6 +22,9 @@ package com.kingsrook.qqq.backend.core.model.metadata.dashboard; +import java.io.Serializable; +import java.util.LinkedHashMap; +import java.util.Map; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; @@ -38,6 +41,8 @@ public class QWidgetMetaData implements QWidgetMetaDataInterface protected Integer gridColumns; protected QCodeReference codeReference; + protected Map defaultValues = new LinkedHashMap<>(); + /******************************************************************************* @@ -242,4 +247,56 @@ public class QWidgetMetaData implements QWidgetMetaDataInterface return (this); } + + + /******************************************************************************* + ** Getter for defaultValues + ** + *******************************************************************************/ + public Map getDefaultValues() + { + return defaultValues; + } + + + + /******************************************************************************* + ** Setter for defaultValues + ** + *******************************************************************************/ + public void setDefaultValues(Map defaultValues) + { + this.defaultValues = defaultValues; + } + + + + /******************************************************************************* + ** Fluent setter for defaultValues + ** + *******************************************************************************/ + public QWidgetMetaData withDefaultValues(Map defaultValues) + { + this.defaultValues = defaultValues; + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for a single defaultValue + ** + *******************************************************************************/ + public QWidgetMetaData withDefaultValue(String key, Serializable value) + { + if(this.defaultValues == null) + { + this.defaultValues = new LinkedHashMap<>(); + } + + this.defaultValues.put(key, value); + + return (this); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/AdornmentType.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/AdornmentType.java index 62abd7db..ad5f3c53 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/AdornmentType.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/AdornmentType.java @@ -34,7 +34,11 @@ public enum AdornmentType { LINK, CHIP, - SIZE; + SIZE, + CODE_EDITOR; + ////////////////////////////////////////////////////////////////////////// + // keep these values in sync with AdornmentType.ts in qqq-frontend-core // + ////////////////////////////////////////////////////////////////////////// diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendTableMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendTableMetaData.java index c5c18d7b..b52ed802 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendTableMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendTableMetaData.java @@ -23,11 +23,16 @@ package com.kingsrook.qqq.backend.core.model.metadata.frontend; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.tables.Capability; import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; @@ -53,6 +58,8 @@ public class QFrontendTableMetaData private List widgets; + private Set capabilities; + ////////////////////////////////////////////////////////////////////////////////// // do not add setters. take values from the source-object in the constructor!! // ////////////////////////////////////////////////////////////////////////////////// @@ -62,7 +69,7 @@ public class QFrontendTableMetaData /******************************************************************************* ** *******************************************************************************/ - public QFrontendTableMetaData(QTableMetaData tableMetaData, boolean includeFields) + public QFrontendTableMetaData(QBackendMetaData backendForTable, QTableMetaData tableMetaData, boolean includeFields) { this.name = tableMetaData.getName(); this.label = tableMetaData.getLabel(); @@ -89,6 +96,62 @@ public class QFrontendTableMetaData { this.widgets = tableMetaData.getWidgets(); } + + setCapabilities(backendForTable, tableMetaData); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private void setCapabilities(QBackendMetaData backend, QTableMetaData table) + { + Set enabledCapabilities = new HashSet<>(); + for(Capability capability : Capability.values()) + { + /////////////////////////////////////////////// + // by default, every table can do everything // + /////////////////////////////////////////////// + boolean hasCapability = true; + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // if the table's backend says the capability is disabled, then by default, then the capability is disabled... // + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////// + if(backend.getDisabledCapabilities().contains(capability)) + { + hasCapability = false; + + ///////////////////////////////////////////////////////////////// + // unless the table overrides that and says that it IS enabled // + ///////////////////////////////////////////////////////////////// + if(table.getEnabledCapabilities().contains(capability)) + { + hasCapability = true; + } + } + else + { + ///////////////////////////////////////////////////////////////////////////////////////// + // if the backend doesn't specify the capability, then disable it if the table says so // + ///////////////////////////////////////////////////////////////////////////////////////// + if(table.getDisabledCapabilities().contains(capability)) + { + hasCapability = false; + } + } + + if(hasCapability) + { + /////////////////////////////////////// + // todo - check if user is allowed!! // + /////////////////////////////////////// + + enabledCapabilities.add(capability); + } + } + + this.capabilities = enabledCapabilities.stream().map(Enum::name).collect(Collectors.toSet()); } @@ -178,4 +241,16 @@ public class QFrontendTableMetaData { return widgets; } + + + + /******************************************************************************* + ** Getter for capabilities + ** + *******************************************************************************/ + public Set getCapabilities() + { + return capabilities; + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/layout/QAppMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/layout/QAppMetaData.java index 36f2a0a2..542d2a79 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/layout/QAppMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/layout/QAppMetaData.java @@ -24,6 +24,9 @@ package com.kingsrook.qqq.backend.core.model.metadata.layout; import java.util.ArrayList; import java.util.List; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; @@ -340,4 +343,38 @@ public class QAppMetaData implements QAppChildMetaData this.addSection(section); return (this); } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public QAppMetaData withSectionOfChildren(QAppSection section, QAppChildMetaData... children) + { + this.addSection(section); + + for(QAppChildMetaData child : children) + { + withChild(child); + if(child instanceof QTableMetaData) + { + section.withTable(child.getName()); + } + else if(child instanceof QProcessMetaData) + { + section.withProcess(child.getName()); + } + else if(child instanceof QReportMetaData) + { + section.withReport(child.getName()); + } + else + { + throw new IllegalArgumentException("Unrecognized child type: " + child.getName()); + } + } + + return (this); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/layout/QAppSection.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/layout/QAppSection.java index 461871b0..b1d0c5b5 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/layout/QAppSection.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/layout/QAppSection.java @@ -22,6 +22,7 @@ package com.kingsrook.qqq.backend.core.model.metadata.layout; +import java.util.ArrayList; import java.util.List; @@ -166,6 +167,22 @@ public class QAppSection + /******************************************************************************* + ** Fluent setter for tables + ** + *******************************************************************************/ + public QAppSection withTable(String tableName) + { + if(this.tables == null) + { + this.tables = new ArrayList<>(); + } + this.tables.add(tableName); + return (this); + } + + + /******************************************************************************* ** Getter for processes ** @@ -200,6 +217,22 @@ public class QAppSection + /******************************************************************************* + ** Fluent setter for processes + ** + *******************************************************************************/ + public QAppSection withProcess(String processName) + { + if(this.processes == null) + { + this.processes = new ArrayList<>(); + } + this.processes.add(processName); + return (this); + } + + + /******************************************************************************* ** Getter for reports ** @@ -234,6 +267,22 @@ public class QAppSection + /******************************************************************************* + ** Fluent setter for reports + ** + *******************************************************************************/ + public QAppSection withReport(String reportName) + { + if(this.reports == null) + { + this.reports = new ArrayList<>(); + } + this.reports.add(reportName); + return (this); + } + + + /******************************************************************************* ** Getter for icon ** diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/layout/QIcon.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/layout/QIcon.java index 92e374c6..cfc2aa40 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/layout/QIcon.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/layout/QIcon.java @@ -34,6 +34,7 @@ package com.kingsrook.qqq.backend.core.model.metadata.layout; public class QIcon { private String name; + private String path; @@ -88,4 +89,38 @@ public class QIcon return (this); } + + + /******************************************************************************* + ** Getter for path + ** + *******************************************************************************/ + public String getPath() + { + return path; + } + + + + /******************************************************************************* + ** Setter for path + ** + *******************************************************************************/ + public void setPath(String path) + { + this.path = path; + } + + + + /******************************************************************************* + ** Fluent setter for path + ** + *******************************************************************************/ + public QIcon withPath(String path) + { + this.path = path; + 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 accce041..280c3c4e 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 @@ -289,6 +289,7 @@ public class QPossibleValueSource *******************************************************************************/ public void setTableName(String tableName) { + this.type = QPossibleValueSourceType.TABLE; this.tableName = tableName; } @@ -300,7 +301,7 @@ public class QPossibleValueSource *******************************************************************************/ public QPossibleValueSource withTableName(String tableName) { - this.tableName = tableName; + setTableName(tableName); return (this); } @@ -446,6 +447,7 @@ public class QPossibleValueSource public void setEnumValues(List> enumValues) { this.enumValues = enumValues; + setType(QPossibleValueSourceType.ENUM); } @@ -456,7 +458,7 @@ public class QPossibleValueSource *******************************************************************************/ public QPossibleValueSource withEnumValues(List> enumValues) { - this.enumValues = enumValues; + setEnumValues(enumValues); return this; } @@ -472,6 +474,7 @@ public class QPossibleValueSource this.enumValues = new ArrayList<>(); } this.enumValues.add(possibleValue); + setType(QPossibleValueSourceType.ENUM); } @@ -512,6 +515,7 @@ public class QPossibleValueSource public void setCustomCodeReference(QCodeReference customCodeReference) { this.customCodeReference = customCodeReference; + setType(QPossibleValueSourceType.CUSTOM); } @@ -522,7 +526,7 @@ public class QPossibleValueSource *******************************************************************************/ public QPossibleValueSource withCustomCodeReference(QCodeReference customCodeReference) { - this.customCodeReference = customCodeReference; + setCustomCodeReference(customCodeReference); return (this); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/AbstractProcessMetaDataBuilder.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/AbstractProcessMetaDataBuilder.java new file mode 100644 index 00000000..cc99b14d --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/AbstractProcessMetaDataBuilder.java @@ -0,0 +1,68 @@ +/* + * 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.metadata.processes; + + +import java.io.Serializable; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class AbstractProcessMetaDataBuilder +{ + protected QProcessMetaData processMetaData; + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public AbstractProcessMetaDataBuilder(QProcessMetaData processMetaData) + { + this.processMetaData = processMetaData; + } + + + + /******************************************************************************* + ** Getter for processMetaData + ** + *******************************************************************************/ + public QProcessMetaData getProcessMetaData() + { + return processMetaData; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + protected void setInputFieldDefaultValue(String fieldName, Serializable value) + { + processMetaData.getInputFields().stream() + .filter(f -> f.getName().equals(fieldName)).findFirst() + .ifPresent(f -> f.setDefaultValue(value)); + } +} 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 83435da8..cc54e526 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 @@ -33,6 +33,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppChildMetaData; import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon; import com.kingsrook.qqq.backend.core.model.metadata.scheduleing.QScheduleMetaData; +import com.kingsrook.qqq.backend.core.processes.implementations.basepull.BasepullConfiguration; /******************************************************************************* @@ -41,10 +42,11 @@ import com.kingsrook.qqq.backend.core.model.metadata.scheduleing.QScheduleMetaDa *******************************************************************************/ public class QProcessMetaData implements QAppChildMetaData { - private String name; - private String label; - private String tableName; - private boolean isHidden = false; + private String name; + private String label; + private String tableName; + private boolean isHidden = false; + private BasepullConfiguration basepullConfiguration; 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 @@ -473,4 +475,38 @@ public class QProcessMetaData implements QAppChildMetaData return (this); } + + + /******************************************************************************* + ** Getter for basepullConfiguration + ** + *******************************************************************************/ + public BasepullConfiguration getBasepullConfiguration() + { + return basepullConfiguration; + } + + + + /******************************************************************************* + ** Setter for basepullConfiguration + ** + *******************************************************************************/ + public void setBasepullConfiguration(BasepullConfiguration basepullConfiguration) + { + this.basepullConfiguration = basepullConfiguration; + } + + + + /******************************************************************************* + ** Fluent setter for basepullConfiguration + ** + *******************************************************************************/ + public QProcessMetaData withBasepullConfiguration(BasepullConfiguration basepullConfiguration) + { + this.basepullConfiguration = basepullConfiguration; + return (this); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/AssociatedScript.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/AssociatedScript.java new file mode 100644 index 00000000..1645e470 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/AssociatedScript.java @@ -0,0 +1,104 @@ +/* + * 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.metadata.tables; + + +import java.io.Serializable; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class AssociatedScript implements Serializable +{ + private String fieldName; + private Serializable scriptTypeId; + + + + /******************************************************************************* + ** Getter for fieldName + ** + *******************************************************************************/ + public String getFieldName() + { + return fieldName; + } + + + + /******************************************************************************* + ** Setter for fieldName + ** + *******************************************************************************/ + public void setFieldName(String fieldName) + { + this.fieldName = fieldName; + } + + + + /******************************************************************************* + ** Fluent setter for fieldName + ** + *******************************************************************************/ + public AssociatedScript withFieldName(String fieldName) + { + this.fieldName = fieldName; + return (this); + } + + + + /******************************************************************************* + ** Getter for scriptTypeId + ** + *******************************************************************************/ + public Serializable getScriptTypeId() + { + return scriptTypeId; + } + + + + /******************************************************************************* + ** Setter for scriptTypeId + ** + *******************************************************************************/ + public void setScriptTypeId(Serializable scriptTypeId) + { + this.scriptTypeId = scriptTypeId; + } + + + + /******************************************************************************* + ** Fluent setter for scriptTypeId + ** + *******************************************************************************/ + public AssociatedScript withScriptTypeId(Serializable scriptTypeId) + { + this.scriptTypeId = scriptTypeId; + return (this); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/Capability.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/Capability.java new file mode 100644 index 00000000..e8321397 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/Capability.java @@ -0,0 +1,41 @@ +/* + * 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.metadata.tables; + + +/******************************************************************************* + ** Things that can be done to tables, fields. + ** + *******************************************************************************/ +public enum Capability +{ + TABLE_QUERY, + TABLE_GET, + TABLE_COUNT, + TABLE_INSERT, + TABLE_UPDATE, + TABLE_DELETE + ////////////////////////////////////////////////////////////////////////// + // keep these values in sync with AdornmentType.ts in qqq-frontend-core // + ////////////////////////////////////////////////////////////////////////// + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QFieldSection.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QFieldSection.java index c1369e65..f62140f4 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QFieldSection.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QFieldSection.java @@ -38,6 +38,8 @@ public class QFieldSection private List fieldNames; private QIcon icon; + private boolean isHidden = false; + /******************************************************************************* @@ -244,4 +246,38 @@ public class QFieldSection return (this); } + + + /******************************************************************************* + ** Getter for isHidden + ** + *******************************************************************************/ + public boolean getIsHidden() + { + return (isHidden); + } + + + + /******************************************************************************* + ** Setter for isHidden + ** + *******************************************************************************/ + public void setIsHidden(boolean isHidden) + { + this.isHidden = isHidden; + } + + + + /******************************************************************************* + ** Fluent Setter for isHidden + ** + *******************************************************************************/ + public QFieldSection withIsHidden(boolean isHidden) + { + this.isHidden = isHidden; + 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 886fc5bc..c7f20b48 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 @@ -26,10 +26,12 @@ import java.io.Serializable; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizer; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.data.QRecordEntity; @@ -78,7 +80,11 @@ public class QTableMetaData implements QAppChildMetaData, Serializable private List sections; - private List widgets; + private List widgets; + private List associatedScripts; + + private Set enabledCapabilities = new HashSet<>(); + private Set disabledCapabilities = new HashSet<>(); @@ -754,6 +760,56 @@ public class QTableMetaData implements QAppChildMetaData, Serializable + /******************************************************************************* + ** Getter for associatedScripts + ** + *******************************************************************************/ + public List getAssociatedScripts() + { + return associatedScripts; + } + + + + /******************************************************************************* + ** Setter for associatedScripts + ** + *******************************************************************************/ + public void setAssociatedScripts(List associatedScripts) + { + this.associatedScripts = associatedScripts; + } + + + + /******************************************************************************* + ** Fluent setter for associatedScripts + ** + *******************************************************************************/ + public QTableMetaData withAssociatedScripts(List associatedScripts) + { + this.associatedScripts = associatedScripts; + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for associatedScripts + ** + *******************************************************************************/ + public QTableMetaData withAssociatedScript(AssociatedScript associatedScript) + { + if(this.associatedScripts == null) + { + this.associatedScripts = new ArrayList(); + } + this.associatedScripts.add(associatedScript); + return (this); + } + + + /******************************************************************************* ** Getter for uniqueKeys ** @@ -802,4 +858,181 @@ public class QTableMetaData implements QAppChildMetaData, Serializable return (this); } + + + /******************************************************************************* + ** Fluently add a section and fields in that section. + *******************************************************************************/ + public QTableMetaData withSectionOfFields(QFieldSection fieldSection, QFieldMetaData... fields) + { + withSection(fieldSection); + + List fieldNames = new ArrayList<>(); + for(QFieldMetaData field : fields) + { + withField(field); + fieldNames.add(field.getName()); + } + + fieldSection.setFieldNames(fieldNames); + + return (this); + } + + + + /******************************************************************************* + ** Getter for enabledCapabilities + ** + *******************************************************************************/ + public Set getEnabledCapabilities() + { + return enabledCapabilities; + } + + + + /******************************************************************************* + ** Setter for enabledCapabilities + ** + *******************************************************************************/ + public void setEnabledCapabilities(Set enabledCapabilities) + { + this.enabledCapabilities = enabledCapabilities; + } + + + + /******************************************************************************* + ** Fluent setter for enabledCapabilities + ** + *******************************************************************************/ + public QTableMetaData withEnabledCapabilities(Set enabledCapabilities) + { + this.enabledCapabilities = enabledCapabilities; + return (this); + } + + + + /******************************************************************************* + ** Alternative fluent setter for enabledCapabilities + ** + *******************************************************************************/ + public QTableMetaData withCapabilities(Set enabledCapabilities) + { + this.enabledCapabilities = enabledCapabilities; + return (this); + } + + + + /******************************************************************************* + ** Alternative fluent setter for a single enabledCapabilities + ** + *******************************************************************************/ + public QTableMetaData withCapability(Capability capability) + { + if(this.enabledCapabilities == null) + { + this.enabledCapabilities = new HashSet<>(); + } + this.enabledCapabilities.add(capability); + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for enabledCapabilities + ** + *******************************************************************************/ + public QTableMetaData withCapabilities(Capability... enabledCapabilities) + { + if(this.enabledCapabilities == null) + { + this.enabledCapabilities = new HashSet<>(); + } + this.enabledCapabilities.addAll(Arrays.stream(enabledCapabilities).toList()); + return (this); + } + + + + /******************************************************************************* + ** Getter for disabledCapabilities + ** + *******************************************************************************/ + public Set getDisabledCapabilities() + { + return disabledCapabilities; + } + + + + /******************************************************************************* + ** Setter for disabledCapabilities + ** + *******************************************************************************/ + public void setDisabledCapabilities(Set disabledCapabilities) + { + this.disabledCapabilities = disabledCapabilities; + } + + + + /******************************************************************************* + ** Fluent setter for disabledCapabilities + ** + *******************************************************************************/ + public QTableMetaData withDisabledCapabilities(Set disabledCapabilities) + { + this.disabledCapabilities = disabledCapabilities; + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for disabledCapabilities + ** + *******************************************************************************/ + public QTableMetaData withoutCapabilities(Capability... disabledCapabilities) + { + if(this.disabledCapabilities == null) + { + this.disabledCapabilities = new HashSet<>(); + } + this.disabledCapabilities.addAll(Arrays.stream(disabledCapabilities).toList()); + return (this); + } + + + + /******************************************************************************* + ** Alternative fluent setter for disabledCapabilities + ** + *******************************************************************************/ + public QTableMetaData withoutCapabilities(Set disabledCapabilities) + { + this.disabledCapabilities = disabledCapabilities; + return (this); + } + + + + /******************************************************************************* + ** Alternative fluent setter for a single disabledCapabilities + ** + *******************************************************************************/ + public QTableMetaData withoutCapability(Capability capability) + { + if(this.disabledCapabilities == null) + { + this.disabledCapabilities = new HashSet<>(); + } + this.disabledCapabilities.add(capability); + return (this); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scripts/Script.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scripts/Script.java new file mode 100644 index 00000000..42fb6fe9 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scripts/Script.java @@ -0,0 +1,282 @@ +/* + * 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.scripts; + + +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; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class Script extends QRecordEntity +{ + public static final String TABLE_NAME = "script"; + + @QField() + private Integer id; + + @QField() + private Instant createDate; + + @QField() + private Instant modifyDate; + + @QField() + private String name; + + @QField(possibleValueSourceName = "scriptType") + private Integer scriptTypeId; + + @QField(possibleValueSourceName = "scriptRevision") + private Integer currentScriptRevisionId; + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public Script() + { + } + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public Script(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; + } + + + + /******************************************************************************* + ** Fluent setter for id + ** + *******************************************************************************/ + public Script withId(Integer id) + { + this.id = id; + return (this); + } + + + + /******************************************************************************* + ** Getter for createDate + ** + *******************************************************************************/ + public Instant getCreateDate() + { + return createDate; + } + + + + /******************************************************************************* + ** Setter for createDate + ** + *******************************************************************************/ + public void setCreateDate(Instant createDate) + { + this.createDate = createDate; + } + + + + /******************************************************************************* + ** Fluent setter for createDate + ** + *******************************************************************************/ + public Script withCreateDate(Instant createDate) + { + this.createDate = createDate; + return (this); + } + + + + /******************************************************************************* + ** Getter for modifyDate + ** + *******************************************************************************/ + public Instant getModifyDate() + { + return modifyDate; + } + + + + /******************************************************************************* + ** Setter for modifyDate + ** + *******************************************************************************/ + public void setModifyDate(Instant modifyDate) + { + this.modifyDate = modifyDate; + } + + + + /******************************************************************************* + ** Fluent setter for modifyDate + ** + *******************************************************************************/ + public Script withModifyDate(Instant modifyDate) + { + this.modifyDate = modifyDate; + return (this); + } + + + + /******************************************************************************* + ** Getter for name + ** + *******************************************************************************/ + public String getName() + { + return name; + } + + + + /******************************************************************************* + ** Setter for name + ** + *******************************************************************************/ + public void setName(String name) + { + this.name = name; + } + + + + /******************************************************************************* + ** Fluent setter for name + ** + *******************************************************************************/ + public Script withName(String name) + { + this.name = name; + return (this); + } + + + + /******************************************************************************* + ** Getter for scriptTypeId + ** + *******************************************************************************/ + public Integer getScriptTypeId() + { + return scriptTypeId; + } + + + + /******************************************************************************* + ** Setter for scriptTypeId + ** + *******************************************************************************/ + public void setScriptTypeId(Integer scriptTypeId) + { + this.scriptTypeId = scriptTypeId; + } + + + + /******************************************************************************* + ** Fluent setter for scriptTypeId + ** + *******************************************************************************/ + public Script withScriptTypeId(Integer scriptTypeId) + { + this.scriptTypeId = scriptTypeId; + return (this); + } + + + + /******************************************************************************* + ** Getter for currentScriptRevisionId + ** + *******************************************************************************/ + public Integer getCurrentScriptRevisionId() + { + return currentScriptRevisionId; + } + + + + /******************************************************************************* + ** Setter for currentScriptRevisionId + ** + *******************************************************************************/ + public void setCurrentScriptRevisionId(Integer currentScriptRevisionId) + { + this.currentScriptRevisionId = currentScriptRevisionId; + } + + + + /******************************************************************************* + ** Fluent setter for currentScriptRevisionId + ** + *******************************************************************************/ + public Script withCurrentScriptRevisionId(Integer currentScriptRevisionId) + { + this.currentScriptRevisionId = currentScriptRevisionId; + return (this); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scripts/ScriptLog.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scripts/ScriptLog.java new file mode 100644 index 00000000..5949f326 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scripts/ScriptLog.java @@ -0,0 +1,482 @@ +/* + * 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.scripts; + + +import java.time.Instant; +import com.kingsrook.qqq.backend.core.model.data.QField; +import com.kingsrook.qqq.backend.core.model.data.QRecordEntity; +import com.kingsrook.qqq.backend.core.model.metadata.fields.DisplayFormat; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class ScriptLog extends QRecordEntity +{ + public static final String TABLE_NAME = "scriptLog"; + + @QField() + private Integer id; + + @QField() + private Instant createDate; + + @QField() + private Instant modifyDate; + + @QField(possibleValueSourceName = "script") + private Integer scriptId; + + @QField(possibleValueSourceName = "scriptRevision") + private Integer scriptRevisionId; + + @QField() + private Instant startTimestamp; + + @QField() + private Instant endTimestamp; + + @QField(displayFormat = DisplayFormat.COMMAS) + private Integer runTimeMillis; + + @QField() + private Boolean hadError; + + @QField() + private String input; + + @QField() + private String output; + + @QField() + private String error; + + + + /******************************************************************************* + ** Getter for id + ** + *******************************************************************************/ + public Integer getId() + { + return id; + } + + + + /******************************************************************************* + ** Setter for id + ** + *******************************************************************************/ + public void setId(Integer id) + { + this.id = id; + } + + + + /******************************************************************************* + ** Fluent setter for id + ** + *******************************************************************************/ + public ScriptLog withId(Integer id) + { + this.id = id; + return (this); + } + + + + /******************************************************************************* + ** Getter for createDate + ** + *******************************************************************************/ + public Instant getCreateDate() + { + return createDate; + } + + + + /******************************************************************************* + ** Setter for createDate + ** + *******************************************************************************/ + public void setCreateDate(Instant createDate) + { + this.createDate = createDate; + } + + + + /******************************************************************************* + ** Fluent setter for createDate + ** + *******************************************************************************/ + public ScriptLog withCreateDate(Instant createDate) + { + this.createDate = createDate; + return (this); + } + + + + /******************************************************************************* + ** Getter for modifyDate + ** + *******************************************************************************/ + public Instant getModifyDate() + { + return modifyDate; + } + + + + /******************************************************************************* + ** Setter for modifyDate + ** + *******************************************************************************/ + public void setModifyDate(Instant modifyDate) + { + this.modifyDate = modifyDate; + } + + + + /******************************************************************************* + ** Fluent setter for modifyDate + ** + *******************************************************************************/ + public ScriptLog withModifyDate(Instant modifyDate) + { + this.modifyDate = modifyDate; + return (this); + } + + + + /******************************************************************************* + ** Getter for scriptId + ** + *******************************************************************************/ + public Integer getScriptId() + { + return scriptId; + } + + + + /******************************************************************************* + ** Setter for scriptId + ** + *******************************************************************************/ + public void setScriptId(Integer scriptId) + { + this.scriptId = scriptId; + } + + + + /******************************************************************************* + ** Fluent setter for scriptId + ** + *******************************************************************************/ + public ScriptLog withScriptId(Integer scriptId) + { + this.scriptId = scriptId; + return (this); + } + + + + /******************************************************************************* + ** Getter for scriptRevisionId + ** + *******************************************************************************/ + public Integer getScriptRevisionId() + { + return scriptRevisionId; + } + + + + /******************************************************************************* + ** Setter for scriptRevisionId + ** + *******************************************************************************/ + public void setScriptRevisionId(Integer scriptRevisionId) + { + this.scriptRevisionId = scriptRevisionId; + } + + + + /******************************************************************************* + ** Fluent setter for scriptRevisionId + ** + *******************************************************************************/ + public ScriptLog withScriptRevisionId(Integer scriptRevisionId) + { + this.scriptRevisionId = scriptRevisionId; + return (this); + } + + + + /******************************************************************************* + ** Getter for startTimestamp + ** + *******************************************************************************/ + public Instant getStartTimestamp() + { + return startTimestamp; + } + + + + /******************************************************************************* + ** Setter for startTimestamp + ** + *******************************************************************************/ + public void setStartTimestamp(Instant startTimestamp) + { + this.startTimestamp = startTimestamp; + } + + + + /******************************************************************************* + ** Fluent setter for startTimestamp + ** + *******************************************************************************/ + public ScriptLog withStartTimestamp(Instant startTimestamp) + { + this.startTimestamp = startTimestamp; + return (this); + } + + + + /******************************************************************************* + ** Getter for endTimestamp + ** + *******************************************************************************/ + public Instant getEndTimestamp() + { + return endTimestamp; + } + + + + /******************************************************************************* + ** Setter for endTimestamp + ** + *******************************************************************************/ + public void setEndTimestamp(Instant endTimestamp) + { + this.endTimestamp = endTimestamp; + } + + + + /******************************************************************************* + ** Fluent setter for endTimestamp + ** + *******************************************************************************/ + public ScriptLog withEndTimestamp(Instant endTimestamp) + { + this.endTimestamp = endTimestamp; + return (this); + } + + + + /******************************************************************************* + ** Getter for runTimeMillis + ** + *******************************************************************************/ + public Integer getRunTimeMillis() + { + return runTimeMillis; + } + + + + /******************************************************************************* + ** Setter for runTimeMillis + ** + *******************************************************************************/ + public void setRunTimeMillis(Integer runTimeMillis) + { + this.runTimeMillis = runTimeMillis; + } + + + + /******************************************************************************* + ** Fluent setter for runTimeMillis + ** + *******************************************************************************/ + public ScriptLog withRunTimeMillis(Integer runTimeMillis) + { + this.runTimeMillis = runTimeMillis; + return (this); + } + + + + /******************************************************************************* + ** Getter for hadError + ** + *******************************************************************************/ + public Boolean getHadError() + { + return hadError; + } + + + + /******************************************************************************* + ** Setter for hadError + ** + *******************************************************************************/ + public void setHadError(Boolean hadError) + { + this.hadError = hadError; + } + + + + /******************************************************************************* + ** Fluent setter for hadError + ** + *******************************************************************************/ + public ScriptLog withHadError(Boolean hadError) + { + this.hadError = hadError; + return (this); + } + + + + /******************************************************************************* + ** Getter for input + ** + *******************************************************************************/ + public String getInput() + { + return input; + } + + + + /******************************************************************************* + ** Setter for input + ** + *******************************************************************************/ + public void setInput(String input) + { + this.input = input; + } + + + + /******************************************************************************* + ** Fluent setter for input + ** + *******************************************************************************/ + public ScriptLog withInput(String input) + { + this.input = input; + return (this); + } + + + + /******************************************************************************* + ** Getter for output + ** + *******************************************************************************/ + public String getOutput() + { + return output; + } + + + + /******************************************************************************* + ** Setter for output + ** + *******************************************************************************/ + public void setOutput(String output) + { + this.output = output; + } + + + + /******************************************************************************* + ** Fluent setter for output + ** + *******************************************************************************/ + public ScriptLog withOutput(String output) + { + this.output = output; + return (this); + } + + + + /******************************************************************************* + ** Getter for error + ** + *******************************************************************************/ + public String getError() + { + return error; + } + + + + /******************************************************************************* + ** Setter for error + ** + *******************************************************************************/ + public void setError(String error) + { + this.error = error; + } + + + + /******************************************************************************* + ** Fluent setter for error + ** + *******************************************************************************/ + public ScriptLog withError(String error) + { + this.error = error; + return (this); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scripts/ScriptLogLine.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scripts/ScriptLogLine.java new file mode 100644 index 00000000..f878e91b --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scripts/ScriptLogLine.java @@ -0,0 +1,259 @@ +/* + * 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.scripts; + + +import java.time.Instant; +import com.kingsrook.qqq.backend.core.model.data.QField; +import com.kingsrook.qqq.backend.core.model.data.QRecordEntity; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class ScriptLogLine extends QRecordEntity +{ + public static final String TABLE_NAME = "scriptLogLine"; + + @QField() + private Integer id; + + @QField() + private Instant createDate; + + @QField() + private Instant modifyDate; + + @QField(possibleValueSourceName = "scriptLog") + private Integer scriptLogId; + + @QField() + private Instant timestamp; + + @QField() + private String text; + + + + /******************************************************************************* + ** Getter for id + ** + *******************************************************************************/ + public Integer getId() + { + return id; + } + + + + /******************************************************************************* + ** Setter for id + ** + *******************************************************************************/ + public void setId(Integer id) + { + this.id = id; + } + + + + /******************************************************************************* + ** Fluent setter for id + ** + *******************************************************************************/ + public ScriptLogLine withId(Integer id) + { + this.id = id; + return (this); + } + + + + /******************************************************************************* + ** Getter for createDate + ** + *******************************************************************************/ + public Instant getCreateDate() + { + return createDate; + } + + + + /******************************************************************************* + ** Setter for createDate + ** + *******************************************************************************/ + public void setCreateDate(Instant createDate) + { + this.createDate = createDate; + } + + + + /******************************************************************************* + ** Fluent setter for createDate + ** + *******************************************************************************/ + public ScriptLogLine withCreateDate(Instant createDate) + { + this.createDate = createDate; + return (this); + } + + + + /******************************************************************************* + ** Getter for modifyDate + ** + *******************************************************************************/ + public Instant getModifyDate() + { + return modifyDate; + } + + + + /******************************************************************************* + ** Setter for modifyDate + ** + *******************************************************************************/ + public void setModifyDate(Instant modifyDate) + { + this.modifyDate = modifyDate; + } + + + + /******************************************************************************* + ** Fluent setter for modifyDate + ** + *******************************************************************************/ + public ScriptLogLine withModifyDate(Instant modifyDate) + { + this.modifyDate = modifyDate; + return (this); + } + + + + /******************************************************************************* + ** Getter for scriptLogId + ** + *******************************************************************************/ + public Integer getScriptLogId() + { + return scriptLogId; + } + + + + /******************************************************************************* + ** Setter for scriptLogId + ** + *******************************************************************************/ + public void setScriptLogId(Integer scriptLogId) + { + this.scriptLogId = scriptLogId; + } + + + + /******************************************************************************* + ** Fluent setter for scriptLogId + ** + *******************************************************************************/ + public ScriptLogLine withScriptLogId(Integer scriptLogId) + { + this.scriptLogId = scriptLogId; + return (this); + } + + + + /******************************************************************************* + ** Getter for timestamp + ** + *******************************************************************************/ + public Instant getTimestamp() + { + return timestamp; + } + + + + /******************************************************************************* + ** Setter for timestamp + ** + *******************************************************************************/ + public void setTimestamp(Instant timestamp) + { + this.timestamp = timestamp; + } + + + + /******************************************************************************* + ** Fluent setter for timestamp + ** + *******************************************************************************/ + public ScriptLogLine withTimestamp(Instant timestamp) + { + this.timestamp = timestamp; + return (this); + } + + + + /******************************************************************************* + ** Getter for text + ** + *******************************************************************************/ + public String getText() + { + return text; + } + + + + /******************************************************************************* + ** Setter for text + ** + *******************************************************************************/ + public void setText(String text) + { + this.text = text; + } + + + + /******************************************************************************* + ** Fluent setter for text + ** + *******************************************************************************/ + public ScriptLogLine withText(String text) + { + this.text = text; + return (this); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scripts/ScriptRevision.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scripts/ScriptRevision.java new file mode 100644 index 00000000..ff8f0d26 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scripts/ScriptRevision.java @@ -0,0 +1,356 @@ +/* + * 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.scripts; + + +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; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class ScriptRevision extends QRecordEntity +{ + public static final String TABLE_NAME = "scriptRevision"; + + @QField() + private Integer id; + + @QField() + private Instant createDate; + + @QField() + private Instant modifyDate; + + @QField(possibleValueSourceName = "script") + private Integer scriptId; + + @QField() + private String contents; + + @QField() + private Integer sequenceNo; + + @QField() + private String commitMessage; + + @QField() + private String author; + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public ScriptRevision() + { + } + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public ScriptRevision(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; + } + + + + /******************************************************************************* + ** Fluent setter for id + ** + *******************************************************************************/ + public ScriptRevision withId(Integer id) + { + this.id = id; + return (this); + } + + + + /******************************************************************************* + ** Getter for createDate + ** + *******************************************************************************/ + public Instant getCreateDate() + { + return createDate; + } + + + + /******************************************************************************* + ** Setter for createDate + ** + *******************************************************************************/ + public void setCreateDate(Instant createDate) + { + this.createDate = createDate; + } + + + + /******************************************************************************* + ** Fluent setter for createDate + ** + *******************************************************************************/ + public ScriptRevision withCreateDate(Instant createDate) + { + this.createDate = createDate; + return (this); + } + + + + /******************************************************************************* + ** Getter for modifyDate + ** + *******************************************************************************/ + public Instant getModifyDate() + { + return modifyDate; + } + + + + /******************************************************************************* + ** Setter for modifyDate + ** + *******************************************************************************/ + public void setModifyDate(Instant modifyDate) + { + this.modifyDate = modifyDate; + } + + + + /******************************************************************************* + ** Fluent setter for modifyDate + ** + *******************************************************************************/ + public ScriptRevision withModifyDate(Instant modifyDate) + { + this.modifyDate = modifyDate; + return (this); + } + + + + /******************************************************************************* + ** Getter for scriptId + ** + *******************************************************************************/ + public Integer getScriptId() + { + return scriptId; + } + + + + /******************************************************************************* + ** Setter for scriptId + ** + *******************************************************************************/ + public void setScriptId(Integer scriptId) + { + this.scriptId = scriptId; + } + + + + /******************************************************************************* + ** Fluent setter for scriptId + ** + *******************************************************************************/ + public ScriptRevision withScriptId(Integer scriptId) + { + this.scriptId = scriptId; + return (this); + } + + + + /******************************************************************************* + ** Getter for contents + ** + *******************************************************************************/ + public String getContents() + { + return contents; + } + + + + /******************************************************************************* + ** Setter for contents + ** + *******************************************************************************/ + public void setContents(String contents) + { + this.contents = contents; + } + + + + /******************************************************************************* + ** Fluent setter for contents + ** + *******************************************************************************/ + public ScriptRevision withContents(String contents) + { + this.contents = contents; + return (this); + } + + + + /******************************************************************************* + ** Getter for sequenceNo + ** + *******************************************************************************/ + public Integer getSequenceNo() + { + return sequenceNo; + } + + + + /******************************************************************************* + ** Setter for sequenceNo + ** + *******************************************************************************/ + public void setSequenceNo(Integer sequenceNo) + { + this.sequenceNo = sequenceNo; + } + + + + /******************************************************************************* + ** Fluent setter for sequenceNo + ** + *******************************************************************************/ + public ScriptRevision withSequenceNo(Integer sequenceNo) + { + this.sequenceNo = sequenceNo; + return (this); + } + + + + /******************************************************************************* + ** Getter for commitMessage + ** + *******************************************************************************/ + public String getCommitMessage() + { + return commitMessage; + } + + + + /******************************************************************************* + ** Setter for commitMessage + ** + *******************************************************************************/ + public void setCommitMessage(String commitMessage) + { + this.commitMessage = commitMessage; + } + + + + /******************************************************************************* + ** Fluent setter for commitMessage + ** + *******************************************************************************/ + public ScriptRevision withCommitMessage(String commitMessage) + { + this.commitMessage = commitMessage; + return (this); + } + + + + /******************************************************************************* + ** Getter for author + ** + *******************************************************************************/ + public String getAuthor() + { + return author; + } + + + + /******************************************************************************* + ** Setter for author + ** + *******************************************************************************/ + public void setAuthor(String author) + { + this.author = author; + } + + + + /******************************************************************************* + ** Fluent setter for author + ** + *******************************************************************************/ + public ScriptRevision withAuthor(String author) + { + this.author = author; + return (this); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scripts/ScriptType.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scripts/ScriptType.java new file mode 100644 index 00000000..6b50d26e --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scripts/ScriptType.java @@ -0,0 +1,259 @@ +/* + * 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.scripts; + + +import java.time.Instant; +import com.kingsrook.qqq.backend.core.model.data.QField; +import com.kingsrook.qqq.backend.core.model.data.QRecordEntity; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class ScriptType extends QRecordEntity +{ + public static final String TABLE_NAME = "scriptType"; + + @QField() + private Integer id; + + @QField() + private Instant createDate; + + @QField() + private Instant modifyDate; + + @QField() + private String name; + + @QField() + private String helpText; + + @QField() + private String sampleCode; + + + + /******************************************************************************* + ** Getter for id + ** + *******************************************************************************/ + public Integer getId() + { + return id; + } + + + + /******************************************************************************* + ** Setter for id + ** + *******************************************************************************/ + public void setId(Integer id) + { + this.id = id; + } + + + + /******************************************************************************* + ** Fluent setter for id + ** + *******************************************************************************/ + public ScriptType withId(Integer id) + { + this.id = id; + return (this); + } + + + + /******************************************************************************* + ** Getter for createDate + ** + *******************************************************************************/ + public Instant getCreateDate() + { + return createDate; + } + + + + /******************************************************************************* + ** Setter for createDate + ** + *******************************************************************************/ + public void setCreateDate(Instant createDate) + { + this.createDate = createDate; + } + + + + /******************************************************************************* + ** Fluent setter for createDate + ** + *******************************************************************************/ + public ScriptType withCreateDate(Instant createDate) + { + this.createDate = createDate; + return (this); + } + + + + /******************************************************************************* + ** Getter for modifyDate + ** + *******************************************************************************/ + public Instant getModifyDate() + { + return modifyDate; + } + + + + /******************************************************************************* + ** Setter for modifyDate + ** + *******************************************************************************/ + public void setModifyDate(Instant modifyDate) + { + this.modifyDate = modifyDate; + } + + + + /******************************************************************************* + ** Fluent setter for modifyDate + ** + *******************************************************************************/ + public ScriptType withModifyDate(Instant modifyDate) + { + this.modifyDate = modifyDate; + return (this); + } + + + + /******************************************************************************* + ** Getter for name + ** + *******************************************************************************/ + public String getName() + { + return name; + } + + + + /******************************************************************************* + ** Setter for name + ** + *******************************************************************************/ + public void setName(String name) + { + this.name = name; + } + + + + /******************************************************************************* + ** Fluent setter for name + ** + *******************************************************************************/ + public ScriptType withName(String name) + { + this.name = name; + return (this); + } + + + + /******************************************************************************* + ** Getter for helpText + ** + *******************************************************************************/ + public String getHelpText() + { + return helpText; + } + + + + /******************************************************************************* + ** Setter for helpText + ** + *******************************************************************************/ + public void setHelpText(String helpText) + { + this.helpText = helpText; + } + + + + /******************************************************************************* + ** Fluent setter for helpText + ** + *******************************************************************************/ + public ScriptType withHelpText(String helpText) + { + this.helpText = helpText; + return (this); + } + + + + /******************************************************************************* + ** Getter for sampleCode + ** + *******************************************************************************/ + public String getSampleCode() + { + return sampleCode; + } + + + + /******************************************************************************* + ** Setter for sampleCode + ** + *******************************************************************************/ + public void setSampleCode(String sampleCode) + { + this.sampleCode = sampleCode; + } + + + + /******************************************************************************* + ** Fluent setter for sampleCode + ** + *******************************************************************************/ + public ScriptType withSampleCode(String sampleCode) + { + this.sampleCode = sampleCode; + return (this); + } + +} 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 new file mode 100644 index 00000000..39c2249d --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scripts/ScriptsMetaDataProvider.java @@ -0,0 +1,217 @@ +/* + * 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.scripts; + + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.data.QRecordEntity; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.fields.AdornmentType; +import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldAdornment; +import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon; +import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource; +import com.kingsrook.qqq.backend.core.model.metadata.tables.Capability; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class ScriptsMetaDataProvider +{ + + /******************************************************************************* + ** + *******************************************************************************/ + public void defineAll(QInstance instance, String backendName, Consumer backendDetailEnricher) throws QException + { + defineStandardScriptsTables(instance, backendName, backendDetailEnricher); + defineStandardScriptsPossibleValueSources(instance); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public void defineStandardScriptsTables(QInstance instance, String backendName, Consumer backendDetailEnricher) throws QException + { + for(QTableMetaData tableMetaData : defineStandardScriptsTables(backendName, backendDetailEnricher)) + { + instance.addTable(tableMetaData); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public void defineStandardScriptsPossibleValueSources(QInstance instance) throws QException + { + instance.addPossibleValueSource(new QPossibleValueSource() + .withName(Script.TABLE_NAME) + .withTableName(Script.TABLE_NAME) + ); + + instance.addPossibleValueSource(new QPossibleValueSource() + .withName(ScriptRevision.TABLE_NAME) + .withTableName(ScriptRevision.TABLE_NAME) + ); + + instance.addPossibleValueSource(new QPossibleValueSource() + .withName(ScriptType.TABLE_NAME) + .withTableName(ScriptType.TABLE_NAME) + ); + + instance.addPossibleValueSource(new QPossibleValueSource() + .withName(ScriptLog.TABLE_NAME) + .withTableName(ScriptLog.TABLE_NAME) + ); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private List defineStandardScriptsTables(String backendName, Consumer backendDetailEnricher) throws QException + { + List rs = new ArrayList<>(); + rs.add(enrich(backendDetailEnricher, defineScriptTypeTable(backendName))); + rs.add(enrich(backendDetailEnricher, defineScriptTable(backendName))); + rs.add(enrich(backendDetailEnricher, defineScriptRevisionTable(backendName))); + rs.add(enrich(backendDetailEnricher, defineScriptLogTable(backendName))); + rs.add(enrich(backendDetailEnricher, defineScriptLogLineTable(backendName))); + return (rs); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private QTableMetaData enrich(Consumer backendDetailEnricher, QTableMetaData table) + { + if(backendDetailEnricher != null) + { + backendDetailEnricher.accept(table); + } + return (table); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private QTableMetaData defineStandardTable(String backendName, String name, Class fieldsFromEntity) throws QException + { + return new QTableMetaData() + .withName(name) + .withBackendName(backendName) + .withRecordLabelFormat("%s") + .withRecordLabelFields("name") + .withPrimaryKeyField("id") + .withFieldsFromEntity(fieldsFromEntity); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private QTableMetaData defineScriptTable(String backendName) throws QException + { + return (defineStandardTable(backendName, Script.TABLE_NAME, Script.class) + .withSection(new QFieldSection("identity", new QIcon().withName("badge"), Tier.T1, List.of("id", "name", "scriptTypeId", "currentScriptRevisionId"))) + .withSection(new QFieldSection("dates", new QIcon().withName("calendar_month"), Tier.T3, List.of("createDate", "modifyDate")))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private QTableMetaData defineScriptTypeTable(String backendName) throws QException + { + QTableMetaData tableMetaData = defineStandardTable(backendName, ScriptType.TABLE_NAME, ScriptType.class) + .withSection(new QFieldSection("identity", new QIcon().withName("badge"), Tier.T1, List.of("id", "name"))) + .withSection(new QFieldSection("details", new QIcon().withName("dataset"), Tier.T2, List.of("helpText", "sampleCode"))) + .withSection(new QFieldSection("dates", new QIcon().withName("calendar_month"), Tier.T3, List.of("createDate", "modifyDate"))); + tableMetaData.getField("sampleCode").withFieldAdornment(new FieldAdornment(AdornmentType.CODE_EDITOR)); + return (tableMetaData); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private QTableMetaData defineScriptRevisionTable(String backendName) throws QException + { + QTableMetaData tableMetaData = defineStandardTable(backendName, ScriptRevision.TABLE_NAME, ScriptRevision.class) + .withoutCapabilities(Capability.TABLE_INSERT, Capability.TABLE_UPDATE, Capability.TABLE_DELETE) + .withRecordLabelFormat("%s v%s") + .withRecordLabelFields(List.of("scriptId", "sequenceNo")) + .withSection(new QFieldSection("identity", new QIcon().withName("badge"), Tier.T1, List.of("id", "scriptId", "sequenceNo"))) + .withSection(new QFieldSection("code", new QIcon().withName("data_object"), Tier.T2, List.of("contents"))) + .withSection(new QFieldSection("changeManagement", new QIcon().withName("history"), Tier.T2, List.of("commitMessage", "author"))) + .withSection(new QFieldSection("dates", new QIcon().withName("calendar_month"), Tier.T3, List.of("createDate", "modifyDate"))); + + tableMetaData.getField("contents").withFieldAdornment(new FieldAdornment(AdornmentType.CODE_EDITOR)); + return (tableMetaData); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private QTableMetaData defineScriptLogTable(String backendName) throws QException + { + return (defineStandardTable(backendName, ScriptLog.TABLE_NAME, ScriptLog.class) + .withRecordLabelFields(List.of("id")) + .withSection(new QFieldSection("identity", new QIcon().withName("badge"), Tier.T1, List.of("id"))) + .withSection(new QFieldSection("script", new QIcon().withName("data_object"), Tier.T2, List.of("scriptId", "scriptRevisionId"))) + .withSection(new QFieldSection("timing", new QIcon().withName("schedule"), Tier.T2, List.of("startTimestamp", "endTimestamp", "runTimeMillis", "createDate", "modifyDate"))) + .withSection(new QFieldSection("error", "Error", new QIcon().withName("error_outline"), Tier.T2, List.of("hadError", "error"))) + .withSection(new QFieldSection("inputOutput", "Input/Output", new QIcon().withName("chat"), Tier.T2, List.of("input", "output")))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private QTableMetaData defineScriptLogLineTable(String backendName) throws QException + { + return (defineStandardTable(backendName, ScriptLogLine.TABLE_NAME, ScriptLogLine.class) + .withRecordLabelFields(List.of("id"))); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/Auth0AuthenticationModule.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/Auth0AuthenticationModule.java index 6b53dd6b..aaa9bc36 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/Auth0AuthenticationModule.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/Auth0AuthenticationModule.java @@ -22,12 +22,16 @@ package com.kingsrook.qqq.backend.core.modules.authentication; +import java.nio.charset.StandardCharsets; import java.security.interfaces.RSAPublicKey; import java.time.Duration; import java.time.Instant; import java.util.Base64; import java.util.Map; import java.util.Optional; +import com.auth0.client.auth.AuthAPI; +import com.auth0.exception.Auth0Exception; +import com.auth0.json.auth.TokenHolder; import com.auth0.jwk.Jwk; import com.auth0.jwk.JwkException; import com.auth0.jwk.JwkProvider; @@ -65,6 +69,7 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface public static final int ID_TOKEN_VALIDATION_INTERVAL_SECONDS = 1800; public static final String AUTH0_ID_TOKEN_KEY = "sessionId"; + public static final String BASIC_AUTH_KEY = "basicAuthString"; public static final String TOKEN_NOT_PROVIDED_ERROR = "Id Token was not provided"; public static final String COULD_NOT_DECODE_ERROR = "Unable to decode id token"; @@ -82,6 +87,43 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface @Override public QSession createSession(QInstance qInstance, Map context) throws QAuthenticationException { + /////////////////////////////////////////////////////////// + // check if we are processing a Basic Auth Session first // + /////////////////////////////////////////////////////////// + if(context.containsKey(BASIC_AUTH_KEY)) + { + Auth0AuthenticationMetaData metaData = (Auth0AuthenticationMetaData) qInstance.getAuthentication(); + AuthAPI auth = new AuthAPI(metaData.getBaseUrl(), metaData.getClientId(), metaData.getClientSecret()); + try + { + ///////////////////////////////////////////////// + // decode the credentials from the header auth // + ///////////////////////////////////////////////// + String base64Credentials = context.get(BASIC_AUTH_KEY).trim(); + byte[] credDecoded = Base64.getDecoder().decode(base64Credentials); + String credentials = new String(credDecoded, StandardCharsets.UTF_8); + + ///////////////////////////////////// + // call auth0 with a login request // + ///////////////////////////////////// + TokenHolder result = auth.login(credentials.split(":")[0], credentials.split(":")[1].toCharArray()) + .setScope("openid email nickname") + .execute(); + + context.put(AUTH0_ID_TOKEN_KEY, result.getIdToken()); + } + catch(Auth0Exception e) + { + //////////////// + // ¯\_(ツ)_/¯ // + //////////////// + String message = "An unknown error occurred during handling basic auth"; + LOG.error(message, e); + throw (new QAuthenticationException(message)); + } + + } + ////////////////////////////////////////////////// // get the jwt id token from the context object // ////////////////////////////////////////////////// diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/metadata/Auth0AuthenticationMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/metadata/Auth0AuthenticationMetaData.java index 2b01799d..e99d4004 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/metadata/Auth0AuthenticationMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/metadata/Auth0AuthenticationMetaData.java @@ -31,6 +31,8 @@ import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType; public class Auth0AuthenticationMetaData extends QAuthenticationMetaData { private String baseUrl; + private String clientId; + private String clientSecret; @@ -76,4 +78,69 @@ public class Auth0AuthenticationMetaData extends QAuthenticationMetaData this.baseUrl = baseUrl; } + + + /******************************************************************************* + ** Fluent setter, override to help fluent flows + *******************************************************************************/ + public Auth0AuthenticationMetaData withClientId(String clientId) + { + setClientId(clientId); + return this; + } + + + + /******************************************************************************* + ** Getter for clientId + ** + *******************************************************************************/ + public String getClientId() + { + return clientId; + } + + + + /******************************************************************************* + ** Setter for clientId + ** + *******************************************************************************/ + public void setClientId(String clientId) + { + this.clientId = clientId; + } + + + + /******************************************************************************* + ** Fluent setter, override to help fluent flows + *******************************************************************************/ + public Auth0AuthenticationMetaData withClientSecret(String clientSecret) + { + setClientSecret(clientSecret); + return this; + } + + + + /******************************************************************************* + ** Getter for clientSecret + ** + *******************************************************************************/ + public String getClientSecret() + { + return clientSecret; + } + + + + /******************************************************************************* + ** Setter for clientSecret + ** + *******************************************************************************/ + public void setClientSecret(String clientSecret) + { + this.clientSecret = clientSecret; + } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/QBackendModuleDispatcher.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/QBackendModuleDispatcher.java index ca95e65e..619dd5d4 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/QBackendModuleDispatcher.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/QBackendModuleDispatcher.java @@ -73,6 +73,7 @@ public class QBackendModuleDispatcher // e.g., backend-core shouldn't need to "know" about the modules. "com.kingsrook.qqq.backend.core.modules.backend.implementations.mock.MockBackendModule", "com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryBackendModule", + "com.kingsrook.qqq.backend.core.modules.backend.implementations.enumeration.EnumerationBackendModule", "com.kingsrook.qqq.backend.module.rdbms.RDBMSBackendModule", "com.kingsrook.qqq.backend.module.filesystem.local.FilesystemBackendModule", "com.kingsrook.qqq.backend.module.filesystem.s3.S3BackendModule", diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/QBackendModuleInterface.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/QBackendModuleInterface.java index 13e6347a..70d51742 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/QBackendModuleInterface.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/QBackendModuleInterface.java @@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.core.modules.backend; import com.kingsrook.qqq.backend.core.actions.interfaces.CountInterface; import com.kingsrook.qqq.backend.core.actions.interfaces.DeleteInterface; +import com.kingsrook.qqq.backend.core.actions.interfaces.GetInterface; import com.kingsrook.qqq.backend.core.actions.interfaces.InsertInterface; import com.kingsrook.qqq.backend.core.actions.interfaces.QueryInterface; import com.kingsrook.qqq.backend.core.actions.interfaces.UpdateInterface; @@ -48,7 +49,10 @@ public interface QBackendModuleInterface /******************************************************************************* ** Method to identify the class used for backend meta data for this module. *******************************************************************************/ - Class getBackendMetaDataClass(); + default Class getBackendMetaDataClass() + { + return (QBackendMetaData.class); + } /******************************************************************************* ** Method to identify the class used for table-backend details for this module. @@ -76,6 +80,15 @@ public interface QBackendModuleInterface return null; } + /******************************************************************************* + ** + *******************************************************************************/ + default GetInterface getGetInterface() + { + throwNotImplemented("Get"); + return null; + } + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/enumeration/EnumerationBackendModule.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/enumeration/EnumerationBackendModule.java new file mode 100644 index 00000000..67650182 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/enumeration/EnumerationBackendModule.java @@ -0,0 +1,71 @@ +/* + * 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.modules.backend.implementations.enumeration; + + +import com.kingsrook.qqq.backend.core.actions.interfaces.QueryInterface; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableBackendDetails; +import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface; + + +/******************************************************************************* + ** Backend module for a table based on a java enum. So we can expose an enum + ** as a table (similar to exposing an enum as a possible value source), with multiple + ** fields in the enum (exposed via getter methods in the enum) as fields in the table. + ** + ** Only supports read-operations, as you can't modify an enum. + *******************************************************************************/ +public class EnumerationBackendModule implements QBackendModuleInterface +{ + + /******************************************************************************* + ** Method where a backend module must be able to provide its type (name). + *******************************************************************************/ + @Override + public String getBackendType() + { + return ("enum"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public Class getTableBackendDetailsClass() + { + return (EnumerationTableBackendDetails.class); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public QueryInterface getQueryInterface() + { + return new EnumerationQueryAction(); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/enumeration/EnumerationQueryAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/enumeration/EnumerationQueryAction.java new file mode 100644 index 00000000..039e0123 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/enumeration/EnumerationQueryAction.java @@ -0,0 +1,84 @@ +/* + * 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.modules.backend.implementations.enumeration; + + +import java.util.ArrayList; +import java.util.List; +import com.kingsrook.qqq.backend.core.actions.interfaces.QueryInterface; +import com.kingsrook.qqq.backend.core.exceptions.QException; +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.data.QRecord; +import com.kingsrook.qqq.backend.core.model.data.QRecordEnum; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.modules.backend.implementations.utils.BackendQueryFilterUtils; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class EnumerationQueryAction implements QueryInterface +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public QueryOutput execute(QueryInput queryInput) throws QException + { + try + { + QTableMetaData table = queryInput.getTable(); + EnumerationTableBackendDetails backendDetails = (EnumerationTableBackendDetails) table.getBackendDetails(); + Class enumClass = backendDetails.getEnumClass(); + QRecordEnum[] values = (QRecordEnum[]) enumClass.getMethod("values").invoke(null); + + ////////////////////////////////////////////// + // note - not good streaming behavior here. // + ////////////////////////////////////////////// + + List recordList = new ArrayList<>(); + for(QRecordEnum value : values) + { + QRecord record = value.toQRecord(); + boolean recordMatches = BackendQueryFilterUtils.doesRecordMatch(queryInput.getFilter(), record); + if(recordMatches) + { + recordList.add(record); + } + } + + BackendQueryFilterUtils.sortRecordList(queryInput.getFilter(), recordList); + recordList = BackendQueryFilterUtils.applySkipAndLimit(queryInput, recordList); + + QueryOutput queryOutput = new QueryOutput(queryInput); + queryOutput.addRecords(recordList); + return queryOutput; + } + catch(Exception e) + { + throw (new QException("Error executing query", e)); + } + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/enumeration/EnumerationTableBackendDetails.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/enumeration/EnumerationTableBackendDetails.java new file mode 100644 index 00000000..ee92f8ce --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/enumeration/EnumerationTableBackendDetails.java @@ -0,0 +1,82 @@ +/* + * 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.modules.backend.implementations.enumeration; + + +import com.kingsrook.qqq.backend.core.model.data.QRecordEnum; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableBackendDetails; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class EnumerationTableBackendDetails extends QTableBackendDetails +{ + private Class enumClass; + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public EnumerationTableBackendDetails() + { + super(); + setBackendType(EnumerationBackendModule.class); + } + + + + /******************************************************************************* + ** Getter for enumClass + ** + *******************************************************************************/ + public Class getEnumClass() + { + return enumClass; + } + + + + /******************************************************************************* + ** Setter for enumClass + ** + *******************************************************************************/ + public void setEnumClass(Class enumClass) + { + this.enumClass = enumClass; + } + + + + /******************************************************************************* + ** Fluent setter for enumClass + ** + *******************************************************************************/ + public EnumerationTableBackendDetails withEnumClass(Class enumClass) + { + this.enumClass = enumClass; + return (this); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryBackendModule.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryBackendModule.java index 30346a7f..cb423dc5 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryBackendModule.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryBackendModule.java @@ -27,7 +27,6 @@ import com.kingsrook.qqq.backend.core.actions.interfaces.DeleteInterface; import com.kingsrook.qqq.backend.core.actions.interfaces.InsertInterface; import com.kingsrook.qqq.backend.core.actions.interfaces.QueryInterface; import com.kingsrook.qqq.backend.core.actions.interfaces.UpdateInterface; -import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData; import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface; @@ -53,17 +52,6 @@ public class MemoryBackendModule implements QBackendModuleInterface - /******************************************************************************* - ** Method to identify the class used for backend meta data for this module. - *******************************************************************************/ - @Override - public Class getBackendMetaDataClass() - { - return (QBackendMetaData.class); - } - - - /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java index 54370e6f..153699a2 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java @@ -23,27 +23,21 @@ package com.kingsrook.qqq.backend.core.modules.backend.implementations.memory; import java.io.Serializable; -import java.time.LocalDate; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.concurrent.atomic.AtomicBoolean; import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput; import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput; import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; -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.actions.tables.query.QueryInput; import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput; import com.kingsrook.qqq.backend.core.model.data.QRecord; 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.utils.CollectionUtils; -import com.kingsrook.qqq.backend.core.utils.ValueUtils; -import org.apache.commons.lang.NotImplementedException; +import com.kingsrook.qqq.backend.core.modules.backend.implementations.utils.BackendQueryFilterUtils; /******************************************************************************* @@ -126,7 +120,7 @@ public class MemoryRecordStore for(QRecord qRecord : tableData.values()) { - boolean recordMatches = doesRecordMatch(input.getFilter(), qRecord); + boolean recordMatches = BackendQueryFilterUtils.doesRecordMatch(input.getFilter(), qRecord); if(recordMatches) { @@ -139,349 +133,6 @@ public class MemoryRecordStore - /******************************************************************************* - ** - *******************************************************************************/ - @SuppressWarnings("checkstyle:indentation") - private boolean doesRecordMatch(QQueryFilter filter, QRecord qRecord) - { - if(filter == null || !filter.hasAnyCriteria()) - { - return (true); - } - - ///////////////////////////////////////////////////////////////////////////////////// - // for an AND query, default to a TRUE answer, and we'll &= each criteria's value. // - // for an OR query, default to FALSE, and |= each criteria's value. // - ///////////////////////////////////////////////////////////////////////////////////// - AtomicBoolean recordMatches = new AtomicBoolean(filter.getBooleanOperator().equals(QQueryFilter.BooleanOperator.AND) ? true : false); - - /////////////////////////////////////// - // if there are criteria, apply them // - /////////////////////////////////////// - for(QFilterCriteria criterion : CollectionUtils.nonNullList(filter.getCriteria())) - { - String fieldName = criterion.getFieldName(); - Serializable value = qRecord.getValue(fieldName); - - boolean criterionMatches = switch(criterion.getOperator()) - { - case EQUALS -> testEquals(criterion, value); - case NOT_EQUALS -> !testEquals(criterion, value); - case IN -> testIn(criterion, value); - case NOT_IN -> !testIn(criterion, value); - case IS_BLANK -> testBlank(criterion, value); - case IS_NOT_BLANK -> !testBlank(criterion, value); - case CONTAINS -> testContains(criterion, fieldName, value); - case NOT_CONTAINS -> !testContains(criterion, fieldName, value); - case STARTS_WITH -> testStartsWith(criterion, fieldName, value); - case NOT_STARTS_WITH -> !testStartsWith(criterion, fieldName, value); - case ENDS_WITH -> testEndsWith(criterion, fieldName, value); - case NOT_ENDS_WITH -> !testEndsWith(criterion, fieldName, value); - case GREATER_THAN -> testGreaterThan(criterion, value); - case GREATER_THAN_OR_EQUALS -> testGreaterThan(criterion, value) || testEquals(criterion, value); - case LESS_THAN -> !testGreaterThan(criterion, value) && !testEquals(criterion, value); - case LESS_THAN_OR_EQUALS -> !testGreaterThan(criterion, value); - case BETWEEN -> - { - QFilterCriteria criteria0 = new QFilterCriteria().withValues(criterion.getValues()); - QFilterCriteria criteria1 = new QFilterCriteria().withValues(new ArrayList<>(criterion.getValues())); - criteria1.getValues().remove(0); - yield (testGreaterThan(criteria0, value) || testEquals(criteria0, value)) && (!testGreaterThan(criteria1, value) || testEquals(criteria1, value)); - } - case NOT_BETWEEN -> - { - QFilterCriteria criteria0 = new QFilterCriteria().withValues(criterion.getValues()); - QFilterCriteria criteria1 = new QFilterCriteria().withValues(criterion.getValues()); - criteria1.getValues().remove(0); - yield !(testGreaterThan(criteria0, value) || testEquals(criteria0, value)) && (!testGreaterThan(criteria1, value) || testEquals(criteria1, value)); - } - }; - - /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - // add this new value to the existing recordMatches value - and if we can short circuit the remaining checks, do so. // - /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - Boolean shortCircuitValue = applyBooleanOperator(recordMatches, criterionMatches, filter.getBooleanOperator()); - if(shortCircuitValue != null) - { - return (shortCircuitValue); - } - } - - //////////////////////////////////////// - // apply sub-filters if there are any // - //////////////////////////////////////// - for(QQueryFilter subFilter : CollectionUtils.nonNullList(filter.getSubFilters())) - { - boolean subFilterMatches = doesRecordMatch(subFilter, qRecord); - - /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - // add this new value to the existing recordMatches value - and if we can short circuit the remaining checks, do so. // - /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - Boolean shortCircuitValue = applyBooleanOperator(recordMatches, subFilterMatches, filter.getBooleanOperator()); - if(shortCircuitValue != null) - { - return (shortCircuitValue); - } - } - - return (recordMatches.getPlain()); - } - - - - /******************************************************************************* - ** Based on an incoming boolean value (accumulator), a new value, and a boolean - ** operator, update the accumulator, and if we can then short-circuit remaining - ** operations, return a true or false. Returning null means to keep going. - *******************************************************************************/ - private Boolean applyBooleanOperator(AtomicBoolean accumulator, boolean newValue, QQueryFilter.BooleanOperator booleanOperator) - { - boolean accumulatorValue = accumulator.getPlain(); - if(booleanOperator.equals(QQueryFilter.BooleanOperator.AND)) - { - accumulatorValue &= newValue; - if(!accumulatorValue) - { - return (false); - } - } - else - { - accumulatorValue |= newValue; - if(accumulatorValue) - { - return (true); - } - } - - accumulator.set(accumulatorValue); - return (null); - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - private boolean testBlank(QFilterCriteria criterion, Serializable value) - { - if(value == null) - { - return (true); - } - - if("".equals(ValueUtils.getValueAsString(value))) - { - return (true); - } - - return (false); - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - private boolean testGreaterThan(QFilterCriteria criterion, Serializable value) - { - Serializable criterionValue = criterion.getValues().get(0); - if(criterionValue == null) - { - throw (new IllegalArgumentException("Missing criterion value in query")); - } - - if(value == null) - { - ///////////////////////////////////////////////////////////////////////////////////// - // a database would say 'false' for if a null column is > a value, so do the same. // - ///////////////////////////////////////////////////////////////////////////////////// - return (false); - } - - if(value instanceof LocalDate valueDate && criterionValue instanceof LocalDate criterionValueDate) - { - return (valueDate.isAfter(criterionValueDate)); - } - - if(value instanceof Number valueNumber && criterionValue instanceof Number criterionValueNumber) - { - return (valueNumber.doubleValue() > criterionValueNumber.doubleValue()); - } - - if(value instanceof LocalDate || criterionValue instanceof LocalDate) - { - LocalDate valueDate; - if(value instanceof LocalDate ld) - { - valueDate = ld; - } - else - { - valueDate = ValueUtils.getValueAsLocalDate(value); - } - - LocalDate criterionDate; - if(criterionValue instanceof LocalDate ld) - { - criterionDate = ld; - } - else - { - criterionDate = ValueUtils.getValueAsLocalDate(criterionValue); - } - - return (valueDate.isAfter(criterionDate)); - } - - throw (new NotImplementedException("Greater/Less Than comparisons are not (yet?) implemented for the supplied types [" + value.getClass().getSimpleName() + "][" + criterionValue.getClass().getSimpleName() + "]")); - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - private boolean testIn(QFilterCriteria criterion, Serializable value) - { - if(!criterion.getValues().contains(value)) - { - return (false); - } - return (true); - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - private boolean testEquals(QFilterCriteria criterion, Serializable value) - { - if(value == null) - { - return (false); - } - - Serializable criteriaValue = criterion.getValues().get(0); - if(value instanceof String && criteriaValue instanceof Number) - { - criteriaValue = String.valueOf(criteriaValue); - } - else if(criteriaValue instanceof String && value instanceof Number) - { - value = String.valueOf(value); - } - - if(!value.equals(criteriaValue)) - { - return (false); - } - return (true); - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - private boolean testContains(QFilterCriteria criterion, String fieldName, Serializable value) - { - String stringValue = getStringFieldValue(value, fieldName, criterion); - String criterionValue = getFirstStringCriterionValue(criterion); - - if(!stringValue.contains(criterionValue)) - { - return (false); - } - - return (true); - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - private boolean testStartsWith(QFilterCriteria criterion, String fieldName, Serializable value) - { - String stringValue = getStringFieldValue(value, fieldName, criterion); - String criterionValue = getFirstStringCriterionValue(criterion); - - if(!stringValue.startsWith(criterionValue)) - { - return (false); - } - - return (true); - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - private boolean testEndsWith(QFilterCriteria criterion, String fieldName, Serializable value) - { - String stringValue = getStringFieldValue(value, fieldName, criterion); - String criterionValue = getFirstStringCriterionValue(criterion); - - if(!stringValue.endsWith(criterionValue)) - { - return (false); - } - - return (true); - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - private String getFirstStringCriterionValue(QFilterCriteria criteria) - { - if(CollectionUtils.nullSafeIsEmpty(criteria.getValues())) - { - throw new IllegalArgumentException("Missing value for [" + criteria.getOperator() + "] criteria on field [" + criteria.getFieldName() + "]"); - } - Serializable value = criteria.getValues().get(0); - if(value == null) - { - return ""; - } - - if(!(value instanceof String stringValue)) - { - throw new ClassCastException("Value [" + value + "] for criteria [" + criteria.getFieldName() + "] is not a String, which is required for the [" + criteria.getOperator() + "] operator."); - } - - return (stringValue); - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - private String getStringFieldValue(Serializable value, String fieldName, QFilterCriteria criterion) - { - if(value == null) - { - return ""; - } - - if(!(value instanceof String stringValue)) - { - throw new ClassCastException("Value [" + value + "] in field [" + fieldName + "] is not a String, which is required for the [" + criterion.getOperator() + "] operator."); - } - - return (stringValue); - } - - - /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/utils/BackendQueryFilterUtils.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/utils/BackendQueryFilterUtils.java new file mode 100644 index 00000000..aa2bdfb1 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/utils/BackendQueryFilterUtils.java @@ -0,0 +1,475 @@ +/* + * 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.modules.backend.implementations.utils; + + +import java.io.Serializable; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; +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.data.QRecord; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import com.kingsrook.qqq.backend.core.utils.ValueUtils; +import org.apache.commons.lang.NotImplementedException; + + +/******************************************************************************* + ** Utility class for backend modules that need to do filter operations. + ** + ** e.g., like an in-memory module, or one that's working with files - basically + ** one that doesn't have filtering provided by the backend (like a database or API). + *******************************************************************************/ +public class BackendQueryFilterUtils +{ + + /******************************************************************************* + ** Test if record matches filter. + *******************************************************************************/ + @SuppressWarnings("checkstyle:indentation") + public static boolean doesRecordMatch(QQueryFilter filter, QRecord qRecord) + { + if(filter == null || !filter.hasAnyCriteria()) + { + return (true); + } + + ///////////////////////////////////////////////////////////////////////////////////// + // for an AND query, default to a TRUE answer, and we'll &= each criteria's value. // + // for an OR query, default to FALSE, and |= each criteria's value. // + ///////////////////////////////////////////////////////////////////////////////////// + AtomicBoolean recordMatches = new AtomicBoolean(filter.getBooleanOperator().equals(QQueryFilter.BooleanOperator.AND) ? true : false); + + /////////////////////////////////////// + // if there are criteria, apply them // + /////////////////////////////////////// + for(QFilterCriteria criterion : CollectionUtils.nonNullList(filter.getCriteria())) + { + String fieldName = criterion.getFieldName(); + Serializable value = qRecord.getValue(fieldName); + + boolean criterionMatches = switch(criterion.getOperator()) + { + case EQUALS -> testEquals(criterion, value); + case NOT_EQUALS -> !testEquals(criterion, value); + case IN -> testIn(criterion, value); + case NOT_IN -> !testIn(criterion, value); + case IS_BLANK -> testBlank(criterion, value); + case IS_NOT_BLANK -> !testBlank(criterion, value); + case CONTAINS -> testContains(criterion, fieldName, value); + case NOT_CONTAINS -> !testContains(criterion, fieldName, value); + case STARTS_WITH -> testStartsWith(criterion, fieldName, value); + case NOT_STARTS_WITH -> !testStartsWith(criterion, fieldName, value); + case ENDS_WITH -> testEndsWith(criterion, fieldName, value); + case NOT_ENDS_WITH -> !testEndsWith(criterion, fieldName, value); + case GREATER_THAN -> testGreaterThan(criterion, value); + case GREATER_THAN_OR_EQUALS -> testGreaterThan(criterion, value) || testEquals(criterion, value); + case LESS_THAN -> !testGreaterThan(criterion, value) && !testEquals(criterion, value); + case LESS_THAN_OR_EQUALS -> !testGreaterThan(criterion, value); + case BETWEEN -> + { + QFilterCriteria criteria0 = new QFilterCriteria().withValues(criterion.getValues()); + QFilterCriteria criteria1 = new QFilterCriteria().withValues(new ArrayList<>(criterion.getValues())); + criteria1.getValues().remove(0); + yield (testGreaterThan(criteria0, value) || testEquals(criteria0, value)) && (!testGreaterThan(criteria1, value) || testEquals(criteria1, value)); + } + case NOT_BETWEEN -> + { + QFilterCriteria criteria0 = new QFilterCriteria().withValues(criterion.getValues()); + QFilterCriteria criteria1 = new QFilterCriteria().withValues(criterion.getValues()); + criteria1.getValues().remove(0); + yield !(testGreaterThan(criteria0, value) || testEquals(criteria0, value)) && (!testGreaterThan(criteria1, value) || testEquals(criteria1, value)); + } + }; + + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // add this new value to the existing recordMatches value - and if we can short circuit the remaining checks, do so. // + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + Boolean shortCircuitValue = applyBooleanOperator(recordMatches, criterionMatches, filter.getBooleanOperator()); + if(shortCircuitValue != null) + { + return (shortCircuitValue); + } + } + + //////////////////////////////////////// + // apply sub-filters if there are any // + //////////////////////////////////////// + for(QQueryFilter subFilter : CollectionUtils.nonNullList(filter.getSubFilters())) + { + boolean subFilterMatches = doesRecordMatch(subFilter, qRecord); + + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // add this new value to the existing recordMatches value - and if we can short circuit the remaining checks, do so. // + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + Boolean shortCircuitValue = applyBooleanOperator(recordMatches, subFilterMatches, filter.getBooleanOperator()); + if(shortCircuitValue != null) + { + return (shortCircuitValue); + } + } + + return (recordMatches.getPlain()); + } + + + + /******************************************************************************* + ** Based on an incoming boolean value (accumulator), a new value, and a boolean + ** operator, update the accumulator, and if we can then short-circuit remaining + ** operations, return a true or false. Returning null means to keep going. + *******************************************************************************/ + private static Boolean applyBooleanOperator(AtomicBoolean accumulator, boolean newValue, QQueryFilter.BooleanOperator booleanOperator) + { + boolean accumulatorValue = accumulator.getPlain(); + if(booleanOperator.equals(QQueryFilter.BooleanOperator.AND)) + { + accumulatorValue &= newValue; + if(!accumulatorValue) + { + return (false); + } + } + else + { + accumulatorValue |= newValue; + if(accumulatorValue) + { + return (true); + } + } + + accumulator.set(accumulatorValue); + return (null); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static boolean testBlank(QFilterCriteria criterion, Serializable value) + { + if(value == null) + { + return (true); + } + + if("".equals(ValueUtils.getValueAsString(value))) + { + return (true); + } + + return (false); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static boolean testGreaterThan(QFilterCriteria criterion, Serializable value) + { + Serializable criterionValue = criterion.getValues().get(0); + if(criterionValue == null) + { + throw (new IllegalArgumentException("Missing criterion value in query")); + } + + if(value == null) + { + ///////////////////////////////////////////////////////////////////////////////////// + // a database would say 'false' for if a null column is > a value, so do the same. // + ///////////////////////////////////////////////////////////////////////////////////// + return (false); + } + + return isGreaterThan(criterionValue, value); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static boolean isGreaterThan(Serializable a, Serializable b) + { + if(Objects.equals(a, b)) + { + return false; + } + + if(b instanceof LocalDate valueDate && a instanceof LocalDate criterionValueDate) + { + return (valueDate.isAfter(criterionValueDate)); + } + + if(b instanceof Number valueNumber && a instanceof Number criterionValueNumber) + { + return (valueNumber.doubleValue() > criterionValueNumber.doubleValue()); + } + + if(b instanceof String valueString && a instanceof String criterionValueString) + { + return (valueString.compareTo(criterionValueString) > 0); + } + + if(b instanceof LocalDate || a instanceof LocalDate) + { + LocalDate valueDate; + if(b instanceof LocalDate ld) + { + valueDate = ld; + } + else + { + valueDate = ValueUtils.getValueAsLocalDate(b); + } + + LocalDate criterionDate; + if(a instanceof LocalDate ld) + { + criterionDate = ld; + } + else + { + criterionDate = ValueUtils.getValueAsLocalDate(a); + } + + return (valueDate.isAfter(criterionDate)); + } + + throw (new NotImplementedException("Greater/Less Than comparisons are not (yet?) implemented for the supplied types [" + b.getClass().getSimpleName() + "][" + a.getClass().getSimpleName() + "]")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static boolean testIn(QFilterCriteria criterion, Serializable value) + { + if(!criterion.getValues().contains(value)) + { + return (false); + } + return (true); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static boolean testEquals(QFilterCriteria criterion, Serializable value) + { + if(value == null) + { + return (false); + } + + Serializable criteriaValue = criterion.getValues().get(0); + if(value instanceof String && criteriaValue instanceof Number) + { + criteriaValue = String.valueOf(criteriaValue); + } + else if(criteriaValue instanceof String && value instanceof Number) + { + value = String.valueOf(value); + } + + if(!value.equals(criteriaValue)) + { + return (false); + } + return (true); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static boolean testContains(QFilterCriteria criterion, String fieldName, Serializable value) + { + String stringValue = getStringFieldValue(value, fieldName, criterion); + String criterionValue = getFirstStringCriterionValue(criterion); + + if(!stringValue.contains(criterionValue)) + { + return (false); + } + + return (true); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static boolean testStartsWith(QFilterCriteria criterion, String fieldName, Serializable value) + { + String stringValue = getStringFieldValue(value, fieldName, criterion); + String criterionValue = getFirstStringCriterionValue(criterion); + + if(!stringValue.startsWith(criterionValue)) + { + return (false); + } + + return (true); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static boolean testEndsWith(QFilterCriteria criterion, String fieldName, Serializable value) + { + String stringValue = getStringFieldValue(value, fieldName, criterion); + String criterionValue = getFirstStringCriterionValue(criterion); + + if(!stringValue.endsWith(criterionValue)) + { + return (false); + } + + return (true); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static String getFirstStringCriterionValue(QFilterCriteria criteria) + { + if(CollectionUtils.nullSafeIsEmpty(criteria.getValues())) + { + throw new IllegalArgumentException("Missing value for [" + criteria.getOperator() + "] criteria on field [" + criteria.getFieldName() + "]"); + } + Serializable value = criteria.getValues().get(0); + if(value == null) + { + return ""; + } + + if(!(value instanceof String stringValue)) + { + throw new ClassCastException("Value [" + value + "] for criteria [" + criteria.getFieldName() + "] is not a String, which is required for the [" + criteria.getOperator() + "] operator."); + } + + return (stringValue); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static String getStringFieldValue(Serializable value, String fieldName, QFilterCriteria criterion) + { + if(value == null) + { + return ""; + } + + if(!(value instanceof String stringValue)) + { + throw new ClassCastException("Value [" + value + "] in field [" + fieldName + "] is not a String, which is required for the [" + criterion.getOperator() + "] operator."); + } + + return (stringValue); + } + + + + /******************************************************************************* + ** Sort list of records based on filter. + *******************************************************************************/ + public static void sortRecordList(QQueryFilter filter, List recordList) + { + if(filter == null || CollectionUtils.nullSafeIsEmpty(filter.getOrderBys())) + { + return; + } + + recordList.sort((a, b) -> + { + for(QFilterOrderBy orderBy : filter.getOrderBys()) + { + Serializable valueA = a.getValue(orderBy.getFieldName()); + Serializable valueB = b.getValue(orderBy.getFieldName()); + if(Objects.equals(valueA, valueB)) + { + continue; + } + else if(isGreaterThan(valueA, valueB) && orderBy.getIsAscending()) + { + return (-1); + } + else + { + return (1); + } + } + + return (0); + }); + } + + + + /******************************************************************************* + ** Apply skip & limit attributes from queryInput to a list of records. + *******************************************************************************/ + public static List applySkipAndLimit(QueryInput queryInput, List recordList) + { + Integer skip = queryInput.getSkip(); + if(skip != null && skip > 0) + { + if(skip < recordList.size()) + { + recordList = recordList.subList(skip, recordList.size()); + } + else + { + recordList.clear(); + } + } + + Integer limit = queryInput.getLimit(); + if(limit != null && limit >= 0 && limit < recordList.size()) + { + recordList = recordList.subList(0, limit); + } + return recordList; + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/basepull/BasepullConfiguration.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/basepull/BasepullConfiguration.java new file mode 100644 index 00000000..e207258d --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/basepull/BasepullConfiguration.java @@ -0,0 +1,247 @@ +/* + * 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.processes.implementations.basepull; + + +import java.io.Serializable; + + +/******************************************************************************* + ** Class for storing all basepull configuration data + ** + *******************************************************************************/ +public class BasepullConfiguration implements Serializable +{ + private String tableName; // the table that stores the basepull timestamps + private String keyField; // the field in the basepull timestamps table that stores the key of the basepull (e.g., a process name) + private String keyValue; // the key applied to the keyField - optional - if not set, process.getName is used. + + private String lastRunTimeFieldName; // the field in the basepull timestamps table that stores the last-run time for the job. + private Integer hoursBackForInitialTimestamp; // for the first-run use-case (where there is no row in the timestamps table), how many hours back in time to look. + + private String timestampField; // the name of the field in the table being queried against the last-run timestamp. + + + + /******************************************************************************* + ** Getter for tableName + ** + *******************************************************************************/ + public String getTableName() + { + return tableName; + } + + + + /******************************************************************************* + ** Setter for tableName + ** + *******************************************************************************/ + public void setTableName(String tableName) + { + this.tableName = tableName; + } + + + + /******************************************************************************* + ** Fluent setter for tableName + ** + *******************************************************************************/ + public BasepullConfiguration withTableName(String tableName) + { + this.tableName = tableName; + return (this); + } + + + + /******************************************************************************* + ** Getter for keyField + ** + *******************************************************************************/ + public String getKeyField() + { + return keyField; + } + + + + /******************************************************************************* + ** Setter for keyField + ** + *******************************************************************************/ + public void setKeyField(String keyField) + { + this.keyField = keyField; + } + + + + /******************************************************************************* + ** Fluent setter for keyField + ** + *******************************************************************************/ + public BasepullConfiguration withKeyField(String keyField) + { + this.keyField = keyField; + return (this); + } + + + + /******************************************************************************* + ** Getter for keyValue + ** + *******************************************************************************/ + public String getKeyValue() + { + return keyValue; + } + + + + /******************************************************************************* + ** Setter for keyValue + ** + *******************************************************************************/ + public void setKeyValue(String keyValue) + { + this.keyValue = keyValue; + } + + + + /******************************************************************************* + ** Fluent setter for keyValue + ** + *******************************************************************************/ + public BasepullConfiguration withKeyValue(String keyValue) + { + this.keyValue = keyValue; + return (this); + } + + + + /******************************************************************************* + ** Getter for lastRunTimeFieldName + ** + *******************************************************************************/ + public String getLastRunTimeFieldName() + { + return lastRunTimeFieldName; + } + + + + /******************************************************************************* + ** Setter for lastRunTimeFieldName + ** + *******************************************************************************/ + public void setLastRunTimeFieldName(String lastRunTimeFieldName) + { + this.lastRunTimeFieldName = lastRunTimeFieldName; + } + + + + /******************************************************************************* + ** Fluent setter for lastRunTimeFieldName + ** + *******************************************************************************/ + public BasepullConfiguration withLastRunTimeFieldName(String lastRunTimeFieldName) + { + this.lastRunTimeFieldName = lastRunTimeFieldName; + return (this); + } + + + + /******************************************************************************* + ** Getter for hoursBackForInitialTimestamp + ** + *******************************************************************************/ + public Integer getHoursBackForInitialTimestamp() + { + return hoursBackForInitialTimestamp; + } + + + + /******************************************************************************* + ** Setter for hoursBackForInitialTimestamp + ** + *******************************************************************************/ + public void setHoursBackForInitialTimestamp(Integer hoursBackForInitialTimestamp) + { + this.hoursBackForInitialTimestamp = hoursBackForInitialTimestamp; + } + + + + /******************************************************************************* + ** Fluent setter for hoursBackForInitialTimestamp + ** + *******************************************************************************/ + public BasepullConfiguration withHoursBackForInitialTimestamp(Integer hoursBackForInitialTimestamp) + { + this.hoursBackForInitialTimestamp = hoursBackForInitialTimestamp; + return (this); + } + + + + /******************************************************************************* + ** Getter for timestampField + ** + *******************************************************************************/ + public String getTimestampField() + { + return timestampField; + } + + + + /******************************************************************************* + ** Setter for timestampField + ** + *******************************************************************************/ + public void setTimestampField(String timestampField) + { + this.timestampField = timestampField; + } + + + + /******************************************************************************* + ** Fluent setter for timestampField + ** + *******************************************************************************/ + public BasepullConfiguration withTimestampField(String timestampField) + { + this.timestampField = timestampField; + return (this); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/basepull/ExtractViaBasepullQueryStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/basepull/ExtractViaBasepullQueryStep.java new file mode 100644 index 00000000..806483af --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/basepull/ExtractViaBasepullQueryStep.java @@ -0,0 +1,102 @@ +/* + * 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.processes.implementations.basepull; + + +import java.util.List; +import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction; +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.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.processes.implementations.etl.streamedwithfrontend.ExtractViaQueryStep; + + +/******************************************************************************* + ** Version of ExtractViaQueryStep that knows how to set up a basepull query. + *******************************************************************************/ +public class ExtractViaBasepullQueryStep extends ExtractViaQueryStep +{ + + /******************************************************************************* + ** + *******************************************************************************/ + protected QQueryFilter getQueryFilter(RunBackendStepInput runBackendStepInput) throws QException + { + ////////////////////////////////////////////////////////////// + // get input query filter or if not found, create a new one // + ////////////////////////////////////////////////////////////// + QQueryFilter queryFilter = new QQueryFilter(); + try + { + queryFilter = super.getQueryFilter(runBackendStepInput); + } + catch(QException qe) + { + /////////////////////////////////////////////////////////////////////////////////////// + // if we catch here, assume that is because there was no default filter, continue on // + /////////////////////////////////////////////////////////////////////////////////////// + } + + /////////////////////////////////////////////////////////////////////////////////////////////////// + // build up a query filter that is against the source table for the given source table timestamp // + // field, finding any records that need processed. // + // query will be for: timestamp > lastRun AND timestamp <= thisRun. // + // then thisRun will be stored, so the next run shouldn't find any records from thisRun. // + /////////////////////////////////////////////////////////////////////////////////////////////////// + queryFilter.addCriteria(new QFilterCriteria() + .withFieldName(runBackendStepInput.getValueString(RunProcessAction.BASEPULL_TIMESTAMP_FIELD)) + .withOperator(QCriteriaOperator.GREATER_THAN) + .withValues(List.of(getLastRunTimeString(runBackendStepInput)))); + + queryFilter.addCriteria(new QFilterCriteria() + .withFieldName(runBackendStepInput.getValueString(RunProcessAction.BASEPULL_TIMESTAMP_FIELD)) + .withOperator(QCriteriaOperator.LESS_THAN_OR_EQUALS) + .withValues(List.of(getThisRunTimeString(runBackendStepInput)))); + + queryFilter.addOrderBy(new QFilterOrderBy(runBackendStepInput.getValueString(RunProcessAction.BASEPULL_TIMESTAMP_FIELD))); + + return (queryFilter); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + protected String getLastRunTimeString(RunBackendStepInput runBackendStepInput) throws QException + { + return (runBackendStepInput.getBasepullLastRunTime().toString()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + protected String getThisRunTimeString(RunBackendStepInput runBackendStepInput) throws QException + { + return (runBackendStepInput.getValueInstant(RunProcessAction.BASEPULL_THIS_RUNTIME_KEY).toString()); + } +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/AbstractExtractStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/AbstractExtractStep.java index ea1dbc58..ce227961 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/AbstractExtractStep.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/AbstractExtractStep.java @@ -26,6 +26,7 @@ import com.kingsrook.qqq.backend.core.actions.processes.BackendStep; import com.kingsrook.qqq.backend.core.actions.reporting.RecordPipe; 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; /******************************************************************************* @@ -49,6 +50,18 @@ public abstract class AbstractExtractStep implements BackendStep + /******************************************************************************* + ** Allow subclasses to do an action before the run begins. + *******************************************************************************/ + public void preRun(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException + { + //////////////////////// + // noop in base class // + //////////////////////// + } + + + /******************************************************************************* ** *******************************************************************************/ 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 a7a86049..2f1cdc05 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 @@ -54,6 +54,20 @@ public class ExtractViaQueryStep extends AbstractExtractStep { public static final String FIELD_SOURCE_TABLE = "sourceTable"; + private QQueryFilter queryFilter; + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void preRun(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException + { + super.preRun(runBackendStepInput, runBackendStepOutput); + queryFilter = getQueryFilter(runBackendStepInput); + } + /******************************************************************************* @@ -66,7 +80,7 @@ public class ExtractViaQueryStep extends AbstractExtractStep QueryInput queryInput = new QueryInput(runBackendStepInput.getInstance()); queryInput.setSession(runBackendStepInput.getSession()); queryInput.setTableName(runBackendStepInput.getValueString(FIELD_SOURCE_TABLE)); - queryInput.setFilter(getQueryFilter(runBackendStepInput)); + queryInput.setFilter(queryFilter); queryInput.setRecordPipe(getRecordPipe()); queryInput.setLimit(getLimit()); queryInput.setAsyncJobCallback(runBackendStepInput.getAsyncJobCallback()); @@ -88,7 +102,7 @@ public class ExtractViaQueryStep extends AbstractExtractStep CountInput countInput = new CountInput(runBackendStepInput.getInstance()); countInput.setSession(runBackendStepInput.getSession()); countInput.setTableName(runBackendStepInput.getValueString(FIELD_SOURCE_TABLE)); - countInput.setFilter(getQueryFilter(runBackendStepInput)); + countInput.setFilter(queryFilter); CountOutput countOutput = new CountAction().execute(countInput); return (countOutput.getCount()); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/LoadViaInsertOrUpdateStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/LoadViaInsertOrUpdateStep.java index 86a7e74a..b75c9edb 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/LoadViaInsertOrUpdateStep.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/LoadViaInsertOrUpdateStep.java @@ -37,6 +37,7 @@ 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.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; /******************************************************************************* @@ -49,6 +50,9 @@ public class LoadViaInsertOrUpdateStep extends AbstractLoadStep { public static final String FIELD_DESTINATION_TABLE = "destinationTable"; + protected List recordsToInsert = null; + protected List recordsToUpdate = null; + /******************************************************************************* @@ -58,22 +62,20 @@ public class LoadViaInsertOrUpdateStep extends AbstractLoadStep @Override public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException { - QTableMetaData tableMetaData = runBackendStepInput.getInstance().getTable(runBackendStepInput.getValueString(FIELD_DESTINATION_TABLE)); - List recordsToInsert = new ArrayList<>(); - List recordsToUpdate = new ArrayList<>(); - for(QRecord record : runBackendStepInput.getRecords()) - { - if(record.getValue(tableMetaData.getPrimaryKeyField()) == null) - { - recordsToInsert.add(record); - } - else - { - recordsToUpdate.add(record); - } - } + evaluateRecords(runBackendStepInput); + insertAndUpdateRecords(runBackendStepInput, runBackendStepOutput); + } - if(!recordsToInsert.isEmpty()) + + + /******************************************************************************* + ** + *******************************************************************************/ + public void insertAndUpdateRecords(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException + { + QTableMetaData tableMetaData = runBackendStepInput.getInstance().getTable(runBackendStepInput.getValueString(FIELD_DESTINATION_TABLE)); + + if(CollectionUtils.nullSafeHasContents(recordsToInsert)) { InsertInput insertInput = new InsertInput(runBackendStepInput.getInstance()); insertInput.setSession(runBackendStepInput.getSession()); @@ -84,7 +86,7 @@ public class LoadViaInsertOrUpdateStep extends AbstractLoadStep runBackendStepOutput.getRecords().addAll(insertOutput.getRecords()); } - if(!recordsToUpdate.isEmpty()) + if(CollectionUtils.nullSafeHasContents(recordsToUpdate)) { UpdateInput updateInput = new UpdateInput(runBackendStepInput.getInstance()); updateInput.setSession(runBackendStepInput.getSession()); @@ -110,4 +112,27 @@ public class LoadViaInsertOrUpdateStep extends AbstractLoadStep return (Optional.of(new InsertAction().openTransaction(insertInput))); } + + + + /******************************************************************************* + ** + *******************************************************************************/ + protected void evaluateRecords(RunBackendStepInput runBackendStepInput) throws QException + { + QTableMetaData tableMetaData = runBackendStepInput.getInstance().getTable(runBackendStepInput.getValueString(FIELD_DESTINATION_TABLE)); + recordsToInsert = new ArrayList<>(); + recordsToUpdate = new ArrayList<>(); + for(QRecord record : runBackendStepInput.getRecords()) + { + if(record.getValue(tableMetaData.getPrimaryKeyField()) == null) + { + recordsToInsert.add(record); + } + else + { + recordsToUpdate.add(record); + } + } + } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLExecuteStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLExecuteStep.java index 66e3f9c6..2f0d9d00 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLExecuteStep.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLExecuteStep.java @@ -28,6 +28,7 @@ import java.util.Optional; import com.kingsrook.qqq.backend.core.actions.QBackendTransaction; 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.processes.RunProcessAction; import com.kingsrook.qqq.backend.core.actions.reporting.RecordPipe; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput; @@ -63,6 +64,7 @@ public class StreamedETLExecuteStep extends BaseStreamedETLStep implements Backe RecordPipe recordPipe = new RecordPipe(); AbstractExtractStep extractStep = getExtractStep(runBackendStepInput); extractStep.setRecordPipe(recordPipe); + extractStep.preRun(runBackendStepInput, runBackendStepOutput); AbstractTransformStep transformStep = getTransformStep(runBackendStepInput); AbstractLoadStep loadStep = getLoadStep(runBackendStepInput); @@ -88,14 +90,26 @@ public class StreamedETLExecuteStep extends BaseStreamedETLStep implements Backe updateRecordsWithDisplayValuesAndPossibleValues(runBackendStepInput, loadedRecordList); runBackendStepOutput.setRecords(loadedRecordList); - //////////////////////////////////////////////////////////////////////////////////////////////////// - // get the process summary from the ... transform step? the load step? each knows some... todo? // - //////////////////////////////////////////////////////////////////////////////////////////////////// - runBackendStepOutput.addValue(StreamedETLWithFrontendProcess.FIELD_PROCESS_SUMMARY, transformStep.doGetProcessSummary(runBackendStepOutput, true)); + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // get the process summary from the load step, if it's a summary-provider -- else, use the transform step (which is always a provider) // + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + if(loadStep instanceof ProcessSummaryProviderInterface provider) + { + runBackendStepOutput.addValue(StreamedETLWithFrontendProcess.FIELD_PROCESS_SUMMARY, provider.doGetProcessSummary(runBackendStepOutput, true)); + } + else + { + runBackendStepOutput.addValue(StreamedETLWithFrontendProcess.FIELD_PROCESS_SUMMARY, transformStep.doGetProcessSummary(runBackendStepOutput, true)); + } transformStep.postRun(runBackendStepInput, runBackendStepOutput); loadStep.postRun(runBackendStepInput, runBackendStepOutput); + ////////////////////////////////////////////////////////////////////////////// + // set the flag to state that the basepull timestamp should be updated now. // + ////////////////////////////////////////////////////////////////////////////// + runBackendStepOutput.addValue(RunProcessAction.BASEPULL_READY_TO_UPDATE_TIMESTAMP_FIELD, true); + ///////////////////// // commit the work // ///////////////////// 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 7dd6c185..7c3c782e 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 @@ -30,6 +30,7 @@ import com.kingsrook.qqq.backend.core.actions.reporting.RecordPipe; 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; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput; import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamed.StreamedETLProcess; import org.apache.logging.log4j.LogManager; @@ -66,6 +67,12 @@ public class StreamedETLPreviewStep extends BaseStreamedETLStep implements Backe return; } + if(runBackendStepInput.getFrontendStepBehavior() != null && runBackendStepInput.getFrontendStepBehavior().equals(RunProcessInput.FrontendStepBehavior.SKIP)) + { + LOG.info("Skipping preview because frontent behavior is [" + RunProcessInput.FrontendStepBehavior.SKIP + "]."); + return; + } + ///////////////////////////////////////////////////////////////// // if we're running inside an automation, then skip this step. // ///////////////////////////////////////////////////////////////// @@ -75,13 +82,21 @@ public class StreamedETLPreviewStep extends BaseStreamedETLStep implements Backe return; } - /////////////////////////////////////////// - // request a count from the extract step // - /////////////////////////////////////////// + ////////////////////////////////////////// + // set up the extract & transform steps // + ////////////////////////////////////////// AbstractExtractStep extractStep = getExtractStep(runBackendStepInput); - Integer recordCount = extractStep.doCount(runBackendStepInput); + RecordPipe recordPipe = new RecordPipe(); + extractStep.setLimit(limit); + extractStep.setRecordPipe(recordPipe); + extractStep.preRun(runBackendStepInput, runBackendStepOutput); + + Integer recordCount = extractStep.doCount(runBackendStepInput); runBackendStepOutput.addValue(StreamedETLProcess.FIELD_RECORD_COUNT, recordCount); + AbstractTransformStep transformStep = getTransformStep(runBackendStepInput); + transformStep.preRun(runBackendStepInput, runBackendStepOutput); + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // if the count is less than the normal limit here, and this process supports validation, then go straight to the validation step // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -93,16 +108,6 @@ public class StreamedETLPreviewStep extends BaseStreamedETLStep implements Backe // return; // } - //////////////////////////////////////////////////////// - // proceed with a doing a limited extract & transform // - //////////////////////////////////////////////////////// - RecordPipe recordPipe = new RecordPipe(); - extractStep.setLimit(limit); - extractStep.setRecordPipe(recordPipe); - - AbstractTransformStep transformStep = getTransformStep(runBackendStepInput); - transformStep.preRun(runBackendStepInput, runBackendStepOutput); - List previewRecordList = new ArrayList<>(); new AsyncRecordPipeLoop().run("StreamedETL>Preview>ExtractStep", PROCESS_OUTPUT_RECORD_LIST_LIMIT, recordPipe, (status) -> { @@ -140,7 +145,6 @@ public class StreamedETLPreviewStep extends BaseStreamedETLStep implements Backe - /******************************************************************************* ** *******************************************************************************/ @@ -154,7 +158,7 @@ public class StreamedETLPreviewStep extends BaseStreamedETLStep implements Backe /////////////////////////////////////////////////////////////////////// // make streamed input & output objects from the run input & outputs // /////////////////////////////////////////////////////////////////////// - StreamedBackendStepInput streamedBackendStepInput = new StreamedBackendStepInput(runBackendStepInput, qRecords); + StreamedBackendStepInput streamedBackendStepInput = new StreamedBackendStepInput(runBackendStepInput, qRecords); StreamedBackendStepOutput streamedBackendStepOutput = new StreamedBackendStepOutput(runBackendStepOutput); ///////////////////////////////////////////////////// diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLValidateStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLValidateStep.java index e10a57df..ac9b9e8c 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLValidateStep.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLValidateStep.java @@ -85,6 +85,7 @@ public class StreamedETLValidateStep extends BaseStreamedETLStep implements Back AbstractExtractStep extractStep = getExtractStep(runBackendStepInput); extractStep.setLimit(null); extractStep.setRecordPipe(recordPipe); + extractStep.preRun(runBackendStepInput, runBackendStepOutput); 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 caaf431d..ebbd80c9 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 @@ -23,11 +23,16 @@ package com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwit import java.io.Serializable; +import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; 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.layout.QIcon; +import com.kingsrook.qqq.backend.core.model.metadata.processes.AbstractProcessMetaDataBuilder; 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; @@ -131,8 +136,8 @@ public class StreamedETLWithFrontendProcess .withField(new QFieldMetaData(FIELD_DESTINATION_TABLE, QFieldType.STRING).withDefaultValue(defaultFieldValues.get(FIELD_DESTINATION_TABLE))) .withField(new QFieldMetaData(FIELD_SUPPORTS_FULL_VALIDATION, QFieldType.BOOLEAN).withDefaultValue(defaultFieldValues.getOrDefault(FIELD_SUPPORTS_FULL_VALIDATION, true))) .withField(new QFieldMetaData(FIELD_DEFAULT_QUERY_FILTER, QFieldType.STRING).withDefaultValue(defaultFieldValues.get(FIELD_DEFAULT_QUERY_FILTER))) - .withField(new QFieldMetaData(FIELD_EXTRACT_CODE, QFieldType.STRING).withDefaultValue(new QCodeReference(extractStepClass))) - .withField(new QFieldMetaData(FIELD_TRANSFORM_CODE, QFieldType.STRING).withDefaultValue(new QCodeReference(transformStepClass))) + .withField(new QFieldMetaData(FIELD_EXTRACT_CODE, QFieldType.STRING).withDefaultValue(extractStepClass == null ? null : new QCodeReference(extractStepClass))) + .withField(new QFieldMetaData(FIELD_TRANSFORM_CODE, QFieldType.STRING).withDefaultValue(transformStepClass == null ? null : new QCodeReference(transformStepClass))) .withField(new QFieldMetaData(FIELD_PREVIEW_MESSAGE, QFieldType.STRING).withDefaultValue(defaultFieldValues.getOrDefault(FIELD_PREVIEW_MESSAGE, DEFAULT_PREVIEW_MESSAGE_FOR_INSERT))) ); @@ -153,7 +158,7 @@ public class StreamedETLWithFrontendProcess .withName(STEP_NAME_EXECUTE) .withCode(new QCodeReference(StreamedETLExecuteStep.class)) .withInputData(new QFunctionInputMetaData() - .withField(new QFieldMetaData(FIELD_LOAD_CODE, QFieldType.STRING).withDefaultValue(new QCodeReference(loadStepClass)))) + .withField(new QFieldMetaData(FIELD_LOAD_CODE, QFieldType.STRING).withDefaultValue(loadStepClass == null ? null : new QCodeReference(loadStepClass)))) .withOutputMetaData(new QFunctionOutputMetaData() .withField(new QFieldMetaData(FIELD_PROCESS_SUMMARY, QFieldType.STRING)) ); @@ -169,4 +174,204 @@ public class StreamedETLWithFrontendProcess .addStep(executeStep) .addStep(resultStep); } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static Builder processMetaDataBuilder() + { + return (new Builder(defineProcessMetaData(null, null, null, Collections.emptyMap()))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static class Builder extends AbstractProcessMetaDataBuilder + { + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public Builder(QProcessMetaData processMetaData) + { + super(processMetaData); + } + + + + /******************************************************************************* + ** Fluent setter for extractStepClass + ** + *******************************************************************************/ + public Builder withExtractStepClass(Class extractStepClass) + { + setInputFieldDefaultValue(FIELD_EXTRACT_CODE, new QCodeReference(extractStepClass)); + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for transformStepClass + ** + *******************************************************************************/ + public Builder withTransformStepClass(Class transformStepClass) + { + setInputFieldDefaultValue(FIELD_TRANSFORM_CODE, new QCodeReference(transformStepClass)); + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for loadStepClass + ** + *******************************************************************************/ + public Builder withLoadStepClass(Class loadStepClass) + { + setInputFieldDefaultValue(FIELD_LOAD_CODE, new QCodeReference(loadStepClass)); + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for sourceTable + ** + *******************************************************************************/ + public Builder withSourceTable(String sourceTable) + { + setInputFieldDefaultValue(FIELD_SOURCE_TABLE, sourceTable); + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for destinationTable + ** + *******************************************************************************/ + public Builder withDestinationTable(String destinationTable) + { + setInputFieldDefaultValue(FIELD_DESTINATION_TABLE, destinationTable); + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for supportsFullValidation + ** + *******************************************************************************/ + public Builder withSupportsFullValidation(Boolean supportsFullValidation) + { + setInputFieldDefaultValue(FIELD_SUPPORTS_FULL_VALIDATION, supportsFullValidation); + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for doFullValidation + ** + *******************************************************************************/ + public Builder withDoFullValidation(Boolean doFullValidation) + { + setInputFieldDefaultValue(FIELD_DO_FULL_VALIDATION, doFullValidation); + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for defaultQueryFilter + ** + *******************************************************************************/ + public Builder withDefaultQueryFilter(QQueryFilter defaultQueryFilter) + { + setInputFieldDefaultValue(FIELD_DEFAULT_QUERY_FILTER, defaultQueryFilter); + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for previewMessage + ** + *******************************************************************************/ + public Builder withPreviewMessage(String previewMessage) + { + setInputFieldDefaultValue(FIELD_PREVIEW_MESSAGE, previewMessage); + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for name + ** + *******************************************************************************/ + public Builder withName(String name) + { + processMetaData.setName(name); + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for label + ** + *******************************************************************************/ + public Builder withLabel(String name) + { + processMetaData.setLabel(name); + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for tableName + ** + *******************************************************************************/ + public Builder withTableName(String tableName) + { + processMetaData.setTableName(tableName); + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for icon + ** + *******************************************************************************/ + public Builder withIcon(QIcon icon) + { + processMetaData.setIcon(icon); + return (this); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public Builder withReviewStepRecordFields(List fieldList) + { + QFrontendStepMetaData reviewStep = processMetaData.getFrontendStep(StreamedETLWithFrontendProcess.STEP_NAME_REVIEW); + for(QFieldMetaData fieldMetaData : fieldList) + { + reviewStep.withRecordListField(fieldMetaData); + } + + return (this); + } + } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/mock/MockBackendStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/mock/MockBackendStep.java index da042f3c..31aa8744 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/mock/MockBackendStep.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/mock/MockBackendStep.java @@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.processes.implementations.mock; import com.kingsrook.qqq.backend.core.actions.processes.BackendStep; +import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction; 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; @@ -64,6 +65,7 @@ public class MockBackendStep implements BackendStep runBackendStepOutput.setValues(runBackendStepInput.getValues()); runBackendStepOutput.addValue(FIELD_MOCK_VALUE, MOCK_VALUE); runBackendStepOutput.addValue("noOfPeopleGreeted", runBackendStepInput.getRecords().size()); + runBackendStepOutput.addValue(RunProcessAction.BASEPULL_READY_TO_UPDATE_TIMESTAMP_FIELD, true); if("there".equalsIgnoreCase(runBackendStepInput.getValueString(FIELD_GREETING_SUFFIX))) { diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/ExecuteReportStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/ExecuteReportStep.java index 40525957..ca0525fd 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/ExecuteReportStep.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/ExecuteReportStep.java @@ -37,6 +37,7 @@ import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutp import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormat; import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportInput; import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData; +import com.kingsrook.qqq.backend.core.utils.StringUtils; /******************************************************************************* @@ -46,6 +47,10 @@ import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData; *******************************************************************************/ public class ExecuteReportStep implements BackendStep { + + /******************************************************************************* + ** + *******************************************************************************/ @Override public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException { @@ -70,10 +75,9 @@ public class ExecuteReportStep implements BackendStep new GenerateReportAction().execute(reportInput); - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_HHmm").withZone(ZoneId.systemDefault()); - String datePart = formatter.format(Instant.now()); + String downloadFileBaseName = getDownloadFileBaseName(runBackendStepInput, report); - runBackendStepOutput.addValue("downloadFileName", report.getLabel() + " " + datePart + ".xlsx"); + runBackendStepOutput.addValue("downloadFileName", downloadFileBaseName + ".xlsx"); runBackendStepOutput.addValue("serverFilePath", tmpFile.getCanonicalPath()); } } @@ -82,4 +86,26 @@ public class ExecuteReportStep implements BackendStep throw (new QException("Error running report", e)); } } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private String getDownloadFileBaseName(RunBackendStepInput runBackendStepInput, QReportMetaData report) + { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd-HHmm").withZone(ZoneId.systemDefault()); + String datePart = formatter.format(Instant.now()); + + String downloadFileBaseName = runBackendStepInput.getValueString("downloadFileBaseName"); + if(!StringUtils.hasContent(downloadFileBaseName)) + { + downloadFileBaseName = report.getLabel(); + } + + downloadFileBaseName = downloadFileBaseName.replaceAll("/", "-"); + + return (downloadFileBaseName + " - " + datePart); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/PrepareReportForRecordStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/PrepareReportForRecordStep.java new file mode 100644 index 00000000..696c2133 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/PrepareReportForRecordStep.java @@ -0,0 +1,124 @@ +/* + * 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.processes.implementations.reports; + + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Iterator; +import com.kingsrook.qqq.backend.core.actions.tables.GetAction; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException; +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.data.QRecord; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; + + +/******************************************************************************* + ** Version of PrepareReportStep for a report that runs off a single record. + *******************************************************************************/ +public class PrepareReportForRecordStep extends PrepareReportStep +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException + { + super.run(runBackendStepInput, runBackendStepOutput); + + ////////////////////////////////////////////////////////////////////////////////// + // look for the recordId having been posted to the process - error if not found // + ////////////////////////////////////////////////////////////////////////////////// + Serializable recordId = null; + if("recordIds".equals(runBackendStepInput.getValueString("recordsParam"))) + { + String recordIdsString = runBackendStepInput.getValueString("recordIds"); + String[] recordIdsArray = recordIdsString.split(","); + if(recordIdsArray.length != 1) + { + throw (new QUserFacingException("Exactly 1 record must be selected as input to this report.")); + } + + recordId = recordIdsArray[0]; + } + else + { + throw (new QUserFacingException("No record was selected as input to this report.")); + } + + ///////////////////////////////////////////////////////////////////////////////////////////// + // look for the recordI input field on the process - put the input recordId in that field. // + // then remove that input field from the process's inputFieldList // + ///////////////////////////////////////////////////////////////////////////////////////////// + @SuppressWarnings("unchecked") + ArrayList inputFieldList = (ArrayList) runBackendStepOutput.getValue("inputFieldList"); + if(CollectionUtils.nullSafeHasContents(inputFieldList)) + { + Iterator inputFieldListIterator = inputFieldList.iterator(); + while(inputFieldListIterator.hasNext()) + { + QFieldMetaData fieldMetaData = inputFieldListIterator.next(); + if(fieldMetaData.getName().equals(RunReportForRecordProcess.FIELD_RECORD_ID)) + { + runBackendStepOutput.addValue(RunReportForRecordProcess.FIELD_RECORD_ID, recordId); + inputFieldListIterator.remove(); + runBackendStepOutput.addValue("inputFieldList", inputFieldList); + break; + } + } + } + + GetInput getInput = new GetInput(runBackendStepInput.getInstance()); + getInput.setSession(runBackendStepInput.getSession()); + getInput.setTableName(runBackendStepInput.getTableName()); + getInput.setPrimaryKey(recordId); + getInput.setShouldGenerateDisplayValues(true); + GetOutput getOutput = new GetAction().execute(getInput); + QRecord record = getOutput.getRecord(); + if(record == null) + { + throw (new QUserFacingException("The selected record for the report was not found.")); + } + + String reportName = runBackendStepInput.getValueString("reportName"); + QReportMetaData report = runBackendStepInput.getInstance().getReport(reportName); + // runBackendStepOutput.addValue("downloadFileBaseName", runBackendStepInput.getTable().getLabel() + " " + record.getRecordLabel()); + runBackendStepOutput.addValue("downloadFileBaseName", report.getLabel() + " - " + record.getRecordLabel()); + + ///////////////////////////////////////////////////////////////////////////////////// + // if there are no more input fields, then remove the INPUT step from the process. // + ///////////////////////////////////////////////////////////////////////////////////// + inputFieldList = (ArrayList) runBackendStepOutput.getValue("inputFieldList"); + if(!CollectionUtils.nullSafeHasContents(inputFieldList)) + { + removeInputStepFromProcess(runBackendStepOutput); + } + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/PrepareReportStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/PrepareReportStep.java index a80af740..47c056b4 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/PrepareReportStep.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/PrepareReportStep.java @@ -43,6 +43,10 @@ import com.kingsrook.qqq.backend.core.utils.StringUtils; *******************************************************************************/ public class PrepareReportStep implements BackendStep { + + /******************************************************************************* + ** + *******************************************************************************/ @Override public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException { @@ -68,12 +72,22 @@ public class PrepareReportStep implements BackendStep } else { - ////////////////////////////////////////////////////////////// - // no input? re-route the process to skip the input screen // - ////////////////////////////////////////////////////////////// - List stepList = new ArrayList<>(runBackendStepOutput.getProcessState().getStepList()); - stepList.removeIf(s -> s.equals(BasicRunReportProcess.STEP_NAME_INPUT)); - runBackendStepOutput.getProcessState().setStepList(stepList); + removeInputStepFromProcess(runBackendStepOutput); } } + + + + /******************************************************************************* + ** + *******************************************************************************/ + protected void removeInputStepFromProcess(RunBackendStepOutput runBackendStepOutput) + { + ////////////////////////////////////////////////////////////// + // no input? re-route the process to skip the input screen // + ////////////////////////////////////////////////////////////// + List stepList = new ArrayList<>(runBackendStepOutput.getProcessState().getStepList()); + stepList.removeIf(s -> s.equals(BasicRunReportProcess.STEP_NAME_INPUT)); + runBackendStepOutput.getProcessState().setStepList(stepList); + } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/RunReportForRecordProcess.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/RunReportForRecordProcess.java new file mode 100644 index 00000000..cd4c5aca --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/RunReportForRecordProcess.java @@ -0,0 +1,167 @@ +/* + * 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.processes.implementations.reports; + + +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.layout.QIcon; +import com.kingsrook.qqq.backend.core.model.metadata.processes.AbstractProcessMetaDataBuilder; +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.QFunctionInputMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData; + + +/******************************************************************************* + ** Definition for Basic process to run a report. + *******************************************************************************/ +public class RunReportForRecordProcess +{ + public static final String PROCESS_NAME = "reports.forRecord"; + + public static final String STEP_NAME_PREPARE = "prepare"; + public static final String STEP_NAME_INPUT = "input"; + public static final String STEP_NAME_EXECUTE = "execute"; + public static final String STEP_NAME_ACCESS = "accessReport"; + + public static final String FIELD_REPORT_NAME = "reportName"; + public static final String FIELD_RECORD_ID = "recordId"; + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static Builder processMetaDataBuilder() + { + return (new Builder(defineProcessMetaData())); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static QProcessMetaData defineProcessMetaData() + { + QStepMetaData prepareStep = new QBackendStepMetaData() + .withName(STEP_NAME_PREPARE) + .withCode(new QCodeReference(PrepareReportForRecordStep.class)) + .withInputData(new QFunctionInputMetaData() + .withField(new QFieldMetaData(FIELD_REPORT_NAME, QFieldType.STRING)) + .withField(new QFieldMetaData(FIELD_RECORD_ID, QFieldType.STRING))); + + QStepMetaData inputStep = new QFrontendStepMetaData() + .withName(STEP_NAME_INPUT) + .withComponent(new QFrontendComponentMetaData().withType(QComponentType.EDIT_FORM)); + + QStepMetaData executeStep = new QBackendStepMetaData() + .withName(STEP_NAME_EXECUTE) + .withCode(new QCodeReference(ExecuteReportStep.class)) + .withInputData(new QFunctionInputMetaData() + .withField(new QFieldMetaData(FIELD_REPORT_NAME, QFieldType.STRING))); + + QStepMetaData accessStep = new QFrontendStepMetaData() + .withName(STEP_NAME_ACCESS) + .withComponent(new QFrontendComponentMetaData().withType(QComponentType.DOWNLOAD_FORM)); + // .withViewField(new QFieldMetaData("outputFile", QFieldType.STRING)) + // .withViewField(new QFieldMetaData("message", QFieldType.STRING)); + + return new QProcessMetaData() + .withName(PROCESS_NAME) + .addStep(prepareStep) + .addStep(inputStep) + .addStep(executeStep) + .addStep(accessStep); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static class Builder extends AbstractProcessMetaDataBuilder + { + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public Builder(QProcessMetaData processMetaData) + { + super(processMetaData); + } + + + + /******************************************************************************* + ** Fluent setter for name + ** + *******************************************************************************/ + public Builder withProcessName(String name) + { + processMetaData.setName(name); + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for tableName + ** + *******************************************************************************/ + public Builder withTableName(String tableName) + { + processMetaData.setTableName(tableName); + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for icon + ** + *******************************************************************************/ + public Builder withIcon(QIcon icon) + { + processMetaData.setIcon(icon); + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for reportName + ** + *******************************************************************************/ + public Builder withReportName(String reportName) + { + setInputFieldDefaultValue(RunReportForRecordProcess.FIELD_REPORT_NAME, reportName); + return (this); + } + } +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/utils/GeneralProcessUtils.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/utils/GeneralProcessUtils.java new file mode 100644 index 00000000..7962ec68 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/utils/GeneralProcessUtils.java @@ -0,0 +1,269 @@ +/* + * 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.processes.utils; + + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput; +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.actions.tables.query.QueryInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput; +import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.utils.ListingHash; + + +/******************************************************************************* + ** Utility methods for working with QQQ records and table actions inside user - + ** defined QQQ processes steps. + *******************************************************************************/ +public class GeneralProcessUtils +{ + + /******************************************************************************* + ** For a list of sourceRecords, + ** lookup records in the foreignTableName, + ** that have their foreignTablePrimaryKeyName in the sourceTableForeignKeyFieldName on the sourceRecords. + ** + ** e.g., for a list of orders (with a clientId field), build a map of client.id => client record + ** via getForeignRecordMap(input, orderList, "clientId", "client", "id") + *******************************************************************************/ + public static Map getForeignRecordMap(AbstractActionInput parentActionInput, List sourceRecords, String sourceTableForeignKeyFieldName, String foreignTableName, String foreignTablePrimaryKeyName) throws QException + { + Map foreignRecordMap = new HashMap<>(); + QueryInput queryInput = new QueryInput(parentActionInput.getInstance()); + queryInput.setSession(parentActionInput.getSession()); + queryInput.setTableName(foreignTableName); + List foreignIds = new ArrayList<>(sourceRecords.stream().map(r -> r.getValue(sourceTableForeignKeyFieldName)).toList()); + + queryInput.setFilter(new QQueryFilter().withCriteria(new QFilterCriteria(foreignTablePrimaryKeyName, QCriteriaOperator.IN, foreignIds))); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + for(QRecord foreignRecord : queryOutput.getRecords()) + { + foreignRecordMap.put(foreignRecord.getValue(foreignTablePrimaryKeyName), foreignRecord); + } + return foreignRecordMap; + } + + + + /******************************************************************************* + ** For a list of sourceRecords, + ** lookup records in the foreignTableName, + ** that have their foreignTableForeignKeyName in the sourceTableForeignKeyFieldName on the sourceRecords. + ** + ** e.g., for a list of orders, build a ListingHash of order.id => List(OrderLine records) + ** via getForeignRecordListingHashMap(input, orderList, "id", "orderLine", "orderId") + *******************************************************************************/ + public static ListingHash getForeignRecordListingHashMap(AbstractActionInput parentActionInput, List sourceRecords, String sourceTableForeignKeyFieldName, String foreignTableName, String foreignTableForeignKeyName) throws QException + { + ListingHash foreignRecordMap = new ListingHash<>(); + QueryInput queryInput = new QueryInput(parentActionInput.getInstance()); + queryInput.setSession(parentActionInput.getSession()); + queryInput.setTableName(foreignTableName); + List foreignIds = new ArrayList<>(sourceRecords.stream().map(r -> r.getValue(sourceTableForeignKeyFieldName)).toList()); + + queryInput.setFilter(new QQueryFilter().withCriteria(new QFilterCriteria(foreignTableForeignKeyName, QCriteriaOperator.IN, foreignIds))); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + for(QRecord foreignRecord : queryOutput.getRecords()) + { + foreignRecordMap.add(foreignRecord.getValue(foreignTableForeignKeyName), foreignRecord); + } + return foreignRecordMap; + } + + + + /******************************************************************************* + ** For a list of sourceRecords, + ** lookup records in the foreignTableName, + ** that have their foreignTablePrimaryKeyName in the sourceTableForeignKeyFieldName on the sourceRecords. + ** and set those foreign records as a value in the sourceRecords. + ** + ** e.g., for a list of orders (with a clientId field), setValue("client", QRecord(client)); + ** via addForeignRecordsToRecordList(input, orderList, "clientId", "client", "id") + *******************************************************************************/ + public static void addForeignRecordsToRecordList(AbstractActionInput parentActionInput, List sourceRecords, String sourceTableForeignKeyFieldName, String foreignTableName, String foreignTablePrimaryKeyName) throws QException + { + Map foreignRecordMap = getForeignRecordMap(parentActionInput, sourceRecords, sourceTableForeignKeyFieldName, foreignTableName, foreignTablePrimaryKeyName); + for(QRecord sourceRecord : sourceRecords) + { + QRecord foreignRecord = foreignRecordMap.get(sourceRecord.getValue(sourceTableForeignKeyFieldName)); + sourceRecord.setValue(foreignTableName, foreignRecord); + } + } + + + + /******************************************************************************* + ** For a list of sourceRecords, + ** lookup records in the foreignTableName, + ** that have their foreignTableForeignKeyName in the sourceTableForeignKeyFieldName on the sourceRecords. + ** + ** e.g., for a list of orders, setValue("orderLine", List(QRecord(orderLine))) + ** via addForeignRecordsListToRecordList(input, orderList, "id", "orderLine", "orderId") + *******************************************************************************/ + public static void addForeignRecordsListToRecordList(AbstractActionInput parentActionInput, List sourceRecords, String sourceTableForeignKeyFieldName, String foreignTableName, String foreignTableForeignKeyName) throws QException + { + ListingHash foreignRecordMap = getForeignRecordListingHashMap(parentActionInput, sourceRecords, sourceTableForeignKeyFieldName, foreignTableName, foreignTableForeignKeyName); + for(QRecord sourceRecord : sourceRecords) + { + List foreignRecordList = foreignRecordMap.get(sourceRecord.getValue(sourceTableForeignKeyFieldName)); + if(foreignRecordList != null) + { + if(foreignRecordList instanceof Serializable s) + { + sourceRecord.setValue(foreignTableName, s); + } + else + { + sourceRecord.setValue(foreignTableName, new ArrayList<>(foreignRecordList)); + } + } + } + } + + + + /******************************************************************************* + ** Run a query on tableName, for where fieldName equals fieldValue, and return + ** the list of QRecords. + *******************************************************************************/ + public static List getRecordListByField(AbstractActionInput parentActionInput, String tableName, String fieldName, Serializable fieldValue) throws QException + { + QueryInput queryInput = new QueryInput(parentActionInput.getInstance()); + queryInput.setSession(parentActionInput.getSession()); + queryInput.setTableName(tableName); + queryInput.setFilter(new QQueryFilter().withCriteria(new QFilterCriteria(fieldName, QCriteriaOperator.EQUALS, List.of(fieldValue)))); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + return (queryOutput.getRecords()); + } + + + + /******************************************************************************* + ** Query to get one record by a unique key value. That field can be the primary + ** key, or any other field on the table. Note, if multiple rows do match the value, + ** only 1 (determined in an unspecified way) is returned. + *******************************************************************************/ + public static Optional getRecordById(AbstractActionInput parentActionInput, String tableName, String fieldName, Serializable fieldValue) throws QException + { + QueryInput queryInput = new QueryInput(parentActionInput.getInstance()); + queryInput.setSession(parentActionInput.getSession()); + queryInput.setTableName(tableName); + queryInput.setFilter(new QQueryFilter().withCriteria(new QFilterCriteria(fieldName, QCriteriaOperator.EQUALS, List.of(fieldValue)))); + queryInput.setLimit(1); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + return (queryOutput.getRecords().stream().findFirst()); + } + + + + /******************************************************************************* + ** Load all rows from a table. + ** + ** Note, this is inherently unsafe, if you were to call it on a table with + ** too many rows... Caveat emptor. + *******************************************************************************/ + public static List loadTable(AbstractActionInput parentActionInput, String tableName) throws QException + { + QueryInput queryInput = new QueryInput(parentActionInput.getInstance()); + queryInput.setSession(parentActionInput.getSession()); + queryInput.setTableName(tableName); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + return (queryOutput.getRecords()); + } + + + + /******************************************************************************* + ** Load all rows from a table, into a map, keyed by the keyFieldName. + ** + ** Note - null values from the key field are NOT put in the map. + ** + ** If multiple values are found for the key, they'll squash each other, and only + ** one random value will appear. + ** + ** Also, note, this is inherently unsafe, if you were to call it on a table with + ** too many rows... Caveat emptor. + *******************************************************************************/ + public static Map loadTableToMap(AbstractActionInput parentActionInput, String tableName, String keyFieldName) throws QException + { + QueryInput queryInput = new QueryInput(parentActionInput.getInstance()); + queryInput.setSession(parentActionInput.getSession()); + queryInput.setTableName(tableName); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + List records = queryOutput.getRecords(); + + Map map = new HashMap<>(); + for(QRecord record : records) + { + Serializable value = record.getValue(keyFieldName); + if(value != null) + { + map.put(value, record); + } + } + return (map); + } + + + + /******************************************************************************* + ** Load all rows from a table, into a ListingHash, keyed by the keyFieldName. + ** + ** Note - null values from the key field are NOT put in the map. + ** + ** The ordering of the records is not specified. + ** + ** Also, note, this is inherently unsafe, if you were to call it on a table with + ** too many rows... Caveat emptor. + *******************************************************************************/ + public static ListingHash loadTableToListingHash(AbstractActionInput parentActionInput, String tableName, String keyFieldName) throws QException + { + QueryInput queryInput = new QueryInput(parentActionInput.getInstance()); + queryInput.setSession(parentActionInput.getSession()); + queryInput.setTableName(tableName); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + List records = queryOutput.getRecords(); + + ListingHash map = new ListingHash<>(); + for(QRecord record : records) + { + Serializable value = record.getValue(keyFieldName); + if(value != null) + { + map.add(value, record); + } + } + return (map); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/JsonUtils.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/JsonUtils.java index 88f01258..919526f2 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/JsonUtils.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/JsonUtils.java @@ -23,9 +23,10 @@ package com.kingsrook.qqq.backend.core.utils; import java.io.IOException; -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; -import java.time.format.DateTimeParseException; +import java.time.Instant; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.core.JsonProcessingException; @@ -230,34 +231,86 @@ public class JsonUtils ** Convert a json object into a QRecord ** *******************************************************************************/ - public static QRecord parseQRecord(JSONObject jsonObject, Map fields) + public static QRecord parseQRecord(JSONObject jsonObject, Map fields, boolean useBackendFieldNames) { QRecord record = new QRecord(); + + FIELDS_LOOP: for(String fieldName : fields.keySet()) { - QFieldMetaData metaData = fields.get(fieldName); - String backendName = metaData.getBackendName() != null ? metaData.getBackendName() : fieldName; - switch(metaData.getType()) + String originalBackendName = null; + try { - case INTEGER -> record.setValue(fieldName, jsonObject.optInt(backendName)); - case DECIMAL -> record.setValue(fieldName, jsonObject.optBigDecimal(backendName, null)); - case BOOLEAN -> record.setValue(fieldName, jsonObject.optBoolean(backendName)); - case DATE_TIME -> + QFieldMetaData metaData = fields.get(fieldName); + String backendName = fieldName; + if(useBackendFieldNames) { - String dateTimeString = jsonObject.optString(backendName); - if(StringUtils.hasContent(dateTimeString)) + backendName = metaData.getBackendName() != null ? metaData.getBackendName() : fieldName; + } + + originalBackendName = backendName; + + ///////////////////////////////////////////////////////////////////////////////////////////////// + // if the field backend name has dots in it, interpret that to mean traversal down sub-objects // + ///////////////////////////////////////////////////////////////////////////////////////////////// + JSONObject jsonObjectToUse = jsonObject; + if(backendName.contains(".")) + { + ArrayList levels = new ArrayList<>(List.of(backendName.split("\\."))); + backendName = levels.remove(levels.size() - 1); + + for(String level : levels) { try { - record.setValue(fieldName, LocalDateTime.parse(dateTimeString, DateTimeFormatter.ISO_ZONED_DATE_TIME)); + jsonObjectToUse = jsonObjectToUse.optJSONObject(level); + if(jsonObjectToUse == null) + { + continue FIELDS_LOOP; + } } - catch(DateTimeParseException dtpe1) + catch(Exception e) { - record.setValue(fieldName, LocalDateTime.parse(dateTimeString, DateTimeFormatter.ISO_DATE_TIME)); + continue FIELDS_LOOP; } } } - default -> record.setValue(fieldName, jsonObject.optString(backendName)); + + if(jsonObjectToUse.isNull(backendName)) + { + record.setValue(fieldName, null); + continue; + } + + switch(metaData.getType()) + { + case INTEGER -> record.setValue(fieldName, jsonObjectToUse.optInt(backendName)); + case DECIMAL -> record.setValue(fieldName, jsonObjectToUse.optBigDecimal(backendName, null)); + case BOOLEAN -> record.setValue(fieldName, jsonObjectToUse.optBoolean(backendName)); + case DATE_TIME -> + { + String dateTimeString = jsonObjectToUse.optString(backendName); + if(StringUtils.hasContent(dateTimeString)) + { + Instant instant = ValueUtils.getValueAsInstant(dateTimeString); + record.setValue(fieldName, instant); + } + } + case DATE -> + { + String dateString = jsonObjectToUse.optString(backendName); + if(StringUtils.hasContent(dateString)) + { + LocalDate localDate = ValueUtils.getValueAsLocalDate(dateString); + record.setValue(fieldName, localDate); + } + } + default -> record.setValue(fieldName, jsonObjectToUse.optString(backendName)); + } + } + catch(Exception e) + { + LOG.debug("Caught exception parsing field [" + fieldName + "] as [" + originalBackendName + "]", e); } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ValueUtils.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ValueUtils.java index 12a6fbff..022c6541 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ValueUtils.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ValueUtils.java @@ -30,6 +30,7 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; import java.time.ZoneId; +import java.time.ZoneOffset; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; import java.util.Calendar; @@ -498,6 +499,22 @@ public class ValueUtils } else { + try + { + return LocalDateTime.parse(s, DateTimeFormatter.ISO_ZONED_DATE_TIME).toInstant(ZoneOffset.UTC); + } + catch(DateTimeParseException e2) + { + try + { + return LocalDateTime.parse(s, DateTimeFormatter.ISO_DATE_TIME).toInstant(ZoneOffset.UTC); + } + catch(Exception e3) + { + // just throw the original + } + } + throw (e); } } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/processes/RunProcessTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/processes/RunProcessTest.java index 692aac89..646d7979 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/processes/RunProcessTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/processes/RunProcessTest.java @@ -23,19 +23,27 @@ package com.kingsrook.qqq.backend.core.actions.processes; import java.io.Serializable; +import java.time.DayOfWeek; +import java.time.Instant; +import java.time.ZoneId; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.UUID; +import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessState; 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.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.actions.tables.query.QueryInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput; 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; @@ -47,6 +55,7 @@ import com.kingsrook.qqq.backend.core.processes.implementations.mock.MockBackend import com.kingsrook.qqq.backend.core.state.StateType; import com.kingsrook.qqq.backend.core.state.UUIDAndTypeStateKey; import com.kingsrook.qqq.backend.core.utils.TestUtils; +import com.kingsrook.qqq.backend.core.utils.ValueUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.junit.jupiter.api.Test; @@ -70,6 +79,80 @@ public class RunProcessTest + /******************************************************************************* + ** + *******************************************************************************/ + @Test + public void testBasepull() throws QException + { + TestCallback callback = new TestCallback(); + RunProcessInput request = new RunProcessInput(TestUtils.defineInstance()); + request.setSession(TestUtils.getMockSession()); + request.setProcessName(TestUtils.PROCESS_NAME_BASEPULL); + request.setCallback(callback); + RunProcessOutput result = new RunProcessAction().execute(request); + assertNotNull(result); + + ////////////////////////////////////////////////////////////////////////////////////////// + // get the last run time and 'this' run time - because the definition states that if no // + // rows found, the last runtime timestamp should be for 24 hours ago // + ////////////////////////////////////////////////////////////////////////////////////////// + Instant lastRunTime = (Instant) result.getValues().get(RunProcessAction.BASEPULL_LAST_RUNTIME_KEY); + Instant thisRunTime = (Instant) result.getValues().get(RunProcessAction.BASEPULL_THIS_RUNTIME_KEY); + assertTrue(thisRunTime.isAfter(lastRunTime), "new run time should be after last run time."); + + DayOfWeek lastRunTimeDayOfWeek = lastRunTime.atZone(ZoneId.systemDefault()).getDayOfWeek(); + DayOfWeek thisRunTimeDayOfWeek = thisRunTime.atZone(ZoneId.systemDefault()).getDayOfWeek(); + thisRunTimeDayOfWeek = thisRunTimeDayOfWeek.minus(1); + assertEquals(lastRunTimeDayOfWeek.getValue(), thisRunTimeDayOfWeek.getValue(), "last and this run times should be the same day after subtracting a day"); + + /////////////////////////////////////////////// + // make sure new stamp stored in backend too // + /////////////////////////////////////////////// + assertEquals(thisRunTime, getBasepullLastRunTime(), "last run time should be properly stored in backend"); + + //////////////////////////////////////////////////// + // run the process one more time and check values // + //////////////////////////////////////////////////// + result = new RunProcessAction().execute(request); + assertNotNull(result); + + //////////////////////////////// + // this should still be after // + //////////////////////////////// + lastRunTime = (Instant) result.getValues().get(RunProcessAction.BASEPULL_LAST_RUNTIME_KEY); + thisRunTime = (Instant) result.getValues().get(RunProcessAction.BASEPULL_THIS_RUNTIME_KEY); + assertTrue(thisRunTime.isAfter(lastRunTime), "new run time should be after last run time."); + + /////////////////////////////////////////////// + // make sure new stamp stored in backend too // + /////////////////////////////////////////////// + assertEquals(thisRunTime, getBasepullLastRunTime(), "last run time should be properly stored in backend"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private Instant getBasepullLastRunTime() throws QException + { + QueryInput queryInput = new QueryInput(TestUtils.defineInstance()); + queryInput.setFilter(new QQueryFilter().withCriteria(new QFilterCriteria() + .withFieldName(TestUtils.BASEPULL_KEY_FIELD_NAME) + .withOperator(QCriteriaOperator.EQUALS) + .withValues(List.of(TestUtils.PROCESS_NAME_BASEPULL)))); + queryInput.setSession(TestUtils.getMockSession()); + queryInput.setTableName(TestUtils.TABLE_NAME_BASEPULL); + + QueryOutput queryOutput = new QueryAction().execute(queryInput); + assertNotNull(queryOutput); + assertEquals(1, queryOutput.getRecords().size(), "Should have one record"); + return (ValueUtils.getValueAsInstant(queryOutput.getRecords().get(0).getValue(TestUtils.BASEPULL_LAST_RUN_TIME_FIELD_NAME))); + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/scripts/ExecuteCodeActionTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/scripts/ExecuteCodeActionTest.java new file mode 100644 index 00000000..400eaf23 --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/scripts/ExecuteCodeActionTest.java @@ -0,0 +1,177 @@ +/* + * 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.actions.scripts; + + +import java.io.Serializable; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import com.kingsrook.qqq.backend.core.actions.scripts.logging.Log4jCodeExecutionLogger; +import com.kingsrook.qqq.backend.core.actions.scripts.logging.NoopCodeExecutionLogger; +import com.kingsrook.qqq.backend.core.actions.scripts.logging.QCodeExecutionLoggerInterface; +import com.kingsrook.qqq.backend.core.actions.scripts.logging.StoreScriptLogAndScriptLogLineExecutionLogger; +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; +import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeOutput; +import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeUsage; +import com.kingsrook.qqq.backend.core.model.scripts.ScriptsMetaDataProvider; +import com.kingsrook.qqq.backend.core.model.session.QSession; +import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryRecordStore; +import com.kingsrook.qqq.backend.core.utils.TestUtils; +import com.kingsrook.qqq.backend.core.utils.ValueUtils; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + + +/******************************************************************************* + ** Unit test for ExecuteCodeAction + *******************************************************************************/ +class ExecuteCodeActionTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @BeforeEach + @AfterEach + void beforeAndAfterEach() + { + MemoryRecordStore.getInstance().reset(); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void test() throws QException + { + QInstance qInstance = TestUtils.defineInstance(); + ExecuteCodeInput executeCodeInput = setupInput(qInstance, Map.of("x", 4), new NoopCodeExecutionLogger()); + ExecuteCodeOutput executeCodeOutput = new ExecuteCodeOutput(); + new ExecuteCodeAction().run(executeCodeInput, executeCodeOutput); + assertEquals(16, executeCodeOutput.getOutput()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private ExecuteCodeInput setupInput(QInstance qInstance, Map context, QCodeExecutionLoggerInterface executionLogger) + { + ExecuteCodeInput executeCodeInput = new ExecuteCodeInput(qInstance); + executeCodeInput.setSession(new QSession()); + executeCodeInput.setCodeReference(new QCodeReference(ScriptInJava.class, QCodeUsage.CUSTOMIZER)); + executeCodeInput.setContext(context); + executeCodeInput.setExecutionLogger(executionLogger); + return executeCodeInput; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testLog4jLogger() throws QException + { + QInstance qInstance = TestUtils.defineInstance(); + ExecuteCodeInput executeCodeInput = setupInput(qInstance, Map.of("x", 4), new Log4jCodeExecutionLogger()); + ExecuteCodeOutput executeCodeOutput = new ExecuteCodeOutput(); + new ExecuteCodeAction().run(executeCodeInput, executeCodeOutput); + assertEquals(16, executeCodeOutput.getOutput()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testTableLogger() throws QException + { + QInstance qInstance = TestUtils.defineInstance(); + new ScriptsMetaDataProvider().defineAll(qInstance, TestUtils.MEMORY_BACKEND_NAME, null); + + ExecuteCodeInput executeCodeInput = setupInput(qInstance, Map.of("x", 4), new StoreScriptLogAndScriptLogLineExecutionLogger(1701, 1702)); + ExecuteCodeOutput executeCodeOutput = new ExecuteCodeOutput(); + new ExecuteCodeAction().run(executeCodeInput, executeCodeOutput); + assertEquals(16, executeCodeOutput.getOutput()); + + List scriptLogRecords = TestUtils.queryTable(qInstance, "scriptLog"); + List scriptLogLineRecords = TestUtils.queryTable(qInstance, "scriptLogLine"); + assertEquals(1, scriptLogRecords.size()); + assertEquals(1701, scriptLogRecords.get(0).getValueInteger("scriptId")); + assertEquals(1702, scriptLogRecords.get(0).getValueInteger("scriptRevisionId")); + assertEquals(1, scriptLogLineRecords.size()); + assertEquals(1, scriptLogLineRecords.get(0).getValueInteger("scriptLogId")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testException() + { + QInstance qInstance = TestUtils.defineInstance(); + ExecuteCodeInput executeCodeInput = setupInput(qInstance, Map.of(), new NoopCodeExecutionLogger()); + ExecuteCodeOutput executeCodeOutput = new ExecuteCodeOutput(); + assertThrows(QCodeException.class, () -> + { + new ExecuteCodeAction().run(executeCodeInput, executeCodeOutput); + }); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static class ScriptInJava implements Function, Serializable> + { + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public Serializable apply(Map context) + { + ((QCodeExecutionLoggerInterface) context.get("logger")).log("Test a log"); + + int x = ValueUtils.getValueAsInteger(context.get("x")); + return (x * x); + } + + } +} \ No newline at end of file diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/scripts/RunAssociatedScriptActionTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/scripts/RunAssociatedScriptActionTest.java new file mode 100644 index 00000000..5d708ed4 --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/scripts/RunAssociatedScriptActionTest.java @@ -0,0 +1,306 @@ +/* + * 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.actions.scripts; + + +import java.io.Serializable; +import java.util.List; +import java.util.Map; +import com.kingsrook.qqq.backend.core.actions.tables.GetAction; +import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.exceptions.QNotFoundException; +import com.kingsrook.qqq.backend.core.model.actions.scripts.RunAssociatedScriptInput; +import com.kingsrook.qqq.backend.core.model.actions.scripts.RunAssociatedScriptOutput; +import com.kingsrook.qqq.backend.core.model.actions.scripts.StoreAssociatedScriptInput; +import com.kingsrook.qqq.backend.core.model.actions.scripts.StoreAssociatedScriptOutput; +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.update.UpdateInput; +import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.code.AssociatedScriptCodeReference; +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.AssociatedScript; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.model.scripts.ScriptsMetaDataProvider; +import com.kingsrook.qqq.backend.core.model.session.QSession; +import com.kingsrook.qqq.backend.core.utils.TestUtils; +import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + + +/******************************************************************************* + ** Unit test for RunAssociatedScriptAction + *******************************************************************************/ +class RunAssociatedScriptActionTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void test() throws QException + { + QInstance instance = setupInstance(); + + insertScript(instance, 1, """ + return "Hello"; + """); + + RunAssociatedScriptInput runAssociatedScriptInput = new RunAssociatedScriptInput(instance); + runAssociatedScriptInput.setSession(new QSession()); + runAssociatedScriptInput.setInputValues(Map.of()); + runAssociatedScriptInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY); + runAssociatedScriptInput.setCodeReference(new AssociatedScriptCodeReference() + .withRecordTable(TestUtils.TABLE_NAME_PERSON_MEMORY) + .withRecordPrimaryKey(1) + .withFieldName("testScriptId") + ); + RunAssociatedScriptOutput runAssociatedScriptOutput = new RunAssociatedScriptOutput(); + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // ok - since the core module doesn't have the javascript language support module as a dep, this action will fail - but at least we can confirm it fails with this specific exception! // + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + assertThatThrownBy(() -> new RunAssociatedScriptAction().run(runAssociatedScriptInput, runAssociatedScriptOutput)) + .isInstanceOf(QException.class) + .hasRootCauseInstanceOf(ClassNotFoundException.class) + .hasRootCauseMessage("com.kingsrook.qqq.languages.javascript.QJavaScriptExecutor"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private QInstance setupInstance() throws QException + { + QInstance instance = TestUtils.defineInstance(); + QTableMetaData table = instance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY) + .withField(new QFieldMetaData("testScriptId", QFieldType.INTEGER)) + .withAssociatedScript(new AssociatedScript() + .withScriptTypeId(1) + .withFieldName("testScriptId") + ); + + new ScriptsMetaDataProvider().defineAll(instance, TestUtils.MEMORY_BACKEND_NAME, null); + + TestUtils.insertRecords(instance, table, List.of( + new QRecord().withValue("id", 1), + new QRecord().withValue("id", 2) + )); + + TestUtils.insertRecords(instance, instance.getTable("scriptType"), List.of( + new QRecord().withValue("id", 1).withValue("name", "Test Script Type") + )); + return instance; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testRecordNotFound() throws QException + { + QInstance instance = setupInstance(); + + RunAssociatedScriptInput runAssociatedScriptInput = new RunAssociatedScriptInput(instance); + runAssociatedScriptInput.setSession(new QSession()); + runAssociatedScriptInput.setInputValues(Map.of()); + runAssociatedScriptInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY); + runAssociatedScriptInput.setCodeReference(new AssociatedScriptCodeReference() + .withRecordTable(TestUtils.TABLE_NAME_PERSON_MEMORY) + .withRecordPrimaryKey(-9999) + .withFieldName("testScriptId") + ); + RunAssociatedScriptOutput runAssociatedScriptOutput = new RunAssociatedScriptOutput(); + + assertThatThrownBy(() -> new RunAssociatedScriptAction().run(runAssociatedScriptInput, runAssociatedScriptOutput)) + .isInstanceOf(QNotFoundException.class) + .hasMessageMatching("The requested record.*was not found.*"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testNoScriptInRecord() throws QException + { + QInstance instance = setupInstance(); + + RunAssociatedScriptInput runAssociatedScriptInput = new RunAssociatedScriptInput(instance); + runAssociatedScriptInput.setSession(new QSession()); + runAssociatedScriptInput.setInputValues(Map.of()); + runAssociatedScriptInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY); + runAssociatedScriptInput.setCodeReference(new AssociatedScriptCodeReference() + .withRecordTable(TestUtils.TABLE_NAME_PERSON_MEMORY) + .withRecordPrimaryKey(1) + .withFieldName("testScriptId") + ); + RunAssociatedScriptOutput runAssociatedScriptOutput = new RunAssociatedScriptOutput(); + + assertThatThrownBy(() -> new RunAssociatedScriptAction().run(runAssociatedScriptInput, runAssociatedScriptOutput)) + .isInstanceOf(QNotFoundException.class) + .hasMessageMatching("The input record.*does not have a script specified for.*"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testBadScriptIdInRecord() throws QException + { + QInstance instance = setupInstance(); + + UpdateInput updateInput = new UpdateInput(instance); + updateInput.setSession(new QSession()); + updateInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY); + updateInput.setRecords(List.of(new QRecord().withValue("id", 1).withValue("testScriptId", -9998))); + new UpdateAction().execute(updateInput); + + RunAssociatedScriptInput runAssociatedScriptInput = new RunAssociatedScriptInput(instance); + runAssociatedScriptInput.setSession(new QSession()); + runAssociatedScriptInput.setInputValues(Map.of()); + runAssociatedScriptInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY); + runAssociatedScriptInput.setCodeReference(new AssociatedScriptCodeReference() + .withRecordTable(TestUtils.TABLE_NAME_PERSON_MEMORY) + .withRecordPrimaryKey(1) + .withFieldName("testScriptId") + ); + RunAssociatedScriptOutput runAssociatedScriptOutput = new RunAssociatedScriptOutput(); + + assertThatThrownBy(() -> new RunAssociatedScriptAction().run(runAssociatedScriptInput, runAssociatedScriptOutput)) + .isInstanceOf(QNotFoundException.class) + .hasMessageMatching("The script for record .* was not found.*"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testNoCurrentScriptRevisionOnScript() throws QException + { + QInstance instance = setupInstance(); + + insertScript(instance, 1, """ + return "Hello"; + """); + + GetInput getInput = new GetInput(instance); + getInput.setSession(new QSession()); + getInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY); + getInput.setPrimaryKey(1); + GetOutput getOutput = new GetAction().execute(getInput); + Integer scriptId = getOutput.getRecord().getValueInteger("testScriptId"); + + UpdateInput updateInput = new UpdateInput(instance); + updateInput.setSession(new QSession()); + updateInput.setTableName("script"); + updateInput.setRecords(List.of(new QRecord().withValue("id", scriptId).withValue("currentScriptRevisionId", null))); + new UpdateAction().execute(updateInput); + + RunAssociatedScriptInput runAssociatedScriptInput = new RunAssociatedScriptInput(instance); + runAssociatedScriptInput.setSession(new QSession()); + runAssociatedScriptInput.setInputValues(Map.of()); + runAssociatedScriptInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY); + runAssociatedScriptInput.setCodeReference(new AssociatedScriptCodeReference() + .withRecordTable(TestUtils.TABLE_NAME_PERSON_MEMORY) + .withRecordPrimaryKey(1) + .withFieldName("testScriptId") + ); + RunAssociatedScriptOutput runAssociatedScriptOutput = new RunAssociatedScriptOutput(); + + assertThatThrownBy(() -> new RunAssociatedScriptAction().run(runAssociatedScriptInput, runAssociatedScriptOutput)) + .isInstanceOf(QNotFoundException.class) + .hasMessageMatching("The script for record .* does not have a current version.*"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testBadCurrentScriptRevisionOnScript() throws QException + { + QInstance instance = setupInstance(); + + insertScript(instance, 1, """ + return "Hello"; + """); + + GetInput getInput = new GetInput(instance); + getInput.setSession(new QSession()); + getInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY); + getInput.setPrimaryKey(1); + GetOutput getOutput = new GetAction().execute(getInput); + Integer scriptId = getOutput.getRecord().getValueInteger("testScriptId"); + + UpdateInput updateInput = new UpdateInput(instance); + updateInput.setSession(new QSession()); + updateInput.setTableName("script"); + updateInput.setRecords(List.of(new QRecord().withValue("id", scriptId).withValue("currentScriptRevisionId", 9997))); + new UpdateAction().execute(updateInput); + + RunAssociatedScriptInput runAssociatedScriptInput = new RunAssociatedScriptInput(instance); + runAssociatedScriptInput.setSession(new QSession()); + runAssociatedScriptInput.setInputValues(Map.of()); + runAssociatedScriptInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY); + runAssociatedScriptInput.setCodeReference(new AssociatedScriptCodeReference() + .withRecordTable(TestUtils.TABLE_NAME_PERSON_MEMORY) + .withRecordPrimaryKey(1) + .withFieldName("testScriptId") + ); + RunAssociatedScriptOutput runAssociatedScriptOutput = new RunAssociatedScriptOutput(); + + assertThatThrownBy(() -> new RunAssociatedScriptAction().run(runAssociatedScriptInput, runAssociatedScriptOutput)) + .isInstanceOf(QNotFoundException.class) + .hasMessageMatching("The current revision of the script for record .* was not found.*"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private void insertScript(QInstance instance, Serializable recordId, String code) throws QException + { + StoreAssociatedScriptInput storeAssociatedScriptInput = new StoreAssociatedScriptInput(instance); + storeAssociatedScriptInput.setSession(new QSession()); + storeAssociatedScriptInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY); + storeAssociatedScriptInput.setRecordPrimaryKey(recordId); + storeAssociatedScriptInput.setCode(code); + storeAssociatedScriptInput.setFieldName("testScriptId"); + StoreAssociatedScriptOutput storeAssociatedScriptOutput = new StoreAssociatedScriptOutput(); + new StoreAssociatedScriptAction().run(storeAssociatedScriptInput, storeAssociatedScriptOutput); + } + +} \ No newline at end of file diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/scripts/StoreAssociatedScriptActionTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/scripts/StoreAssociatedScriptActionTest.java new file mode 100644 index 00000000..8c0cced7 --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/scripts/StoreAssociatedScriptActionTest.java @@ -0,0 +1,172 @@ +/* + * 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.actions.scripts; + + +import java.io.Serializable; +import java.util.List; +import com.kingsrook.qqq.backend.core.actions.tables.GetAction; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.scripts.StoreAssociatedScriptInput; +import com.kingsrook.qqq.backend.core.model.actions.scripts.StoreAssociatedScriptOutput; +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.data.QRecord; +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; +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.scripts.ScriptsMetaDataProvider; +import com.kingsrook.qqq.backend.core.model.session.QSession; +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; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + + +/******************************************************************************* + ** Unit test for StoreAssociatedScriptAction + *******************************************************************************/ +class StoreAssociatedScriptActionTest +{ + /******************************************************************************* + ** + *******************************************************************************/ + @BeforeEach + @AfterEach + void beforeAndAfterEach() + { + MemoryRecordStore.getInstance().reset(); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void test() throws QException + { + QInstance instance = TestUtils.defineInstance(); + QTableMetaData table = instance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY) + .withField(new QFieldMetaData("testScriptId", QFieldType.INTEGER)) + .withAssociatedScript(new AssociatedScript() + .withScriptTypeId(1) + .withFieldName("testScriptId") + ) + .withField(new QFieldMetaData("otherScriptId", QFieldType.INTEGER)) + .withAssociatedScript(new AssociatedScript() + .withScriptTypeId(2) + .withFieldName("otherScriptId") + ); + + new ScriptsMetaDataProvider().defineAll(instance, TestUtils.MEMORY_BACKEND_NAME, null); + + TestUtils.insertRecords(instance, table, List.of( + new QRecord().withValue("id", 1), + new QRecord().withValue("id", 2), + new QRecord().withValue("id", 3) + )); + + TestUtils.insertRecords(instance, instance.getTable("scriptType"), List.of( + new QRecord().withValue("id", 1).withValue("name", "Test Script"), + new QRecord().withValue("id", 2).withValue("name", "Other Script") + )); + + StoreAssociatedScriptInput storeAssociatedScriptInput = new StoreAssociatedScriptInput(instance); + storeAssociatedScriptInput.setSession(new QSession()); + storeAssociatedScriptInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY); + storeAssociatedScriptInput.setRecordPrimaryKey(1); + storeAssociatedScriptInput.setCode("var i = 0;"); + storeAssociatedScriptInput.setCommitMessage("Test commit"); + storeAssociatedScriptInput.setFieldName("testScriptId"); + StoreAssociatedScriptOutput storeAssociatedScriptOutput = new StoreAssociatedScriptOutput(); + + /////////////////////////////////////////////// + // insert 1st version of script for record 1 // + /////////////////////////////////////////////// + new StoreAssociatedScriptAction().run(storeAssociatedScriptInput, storeAssociatedScriptOutput); + assertValueInField(instance, TestUtils.TABLE_NAME_PERSON_MEMORY, 1, "testScriptId", 1); + assertValueInField(instance, "script", 1, "currentScriptRevisionId", 1); + + //////////////////////////////////////////// + // add 2nd version of script for record 1 // + //////////////////////////////////////////// + storeAssociatedScriptInput.setCode("var i = 1;"); + storeAssociatedScriptInput.setCommitMessage("2nd commit"); + new StoreAssociatedScriptAction().run(storeAssociatedScriptInput, storeAssociatedScriptOutput); + assertValueInField(instance, TestUtils.TABLE_NAME_PERSON_MEMORY, 1, "testScriptId", 1); + assertValueInField(instance, "script", 1, "currentScriptRevisionId", 2); + + /////////////////////////////////////////////// + // insert 1st version of script for record 3 // + /////////////////////////////////////////////// + storeAssociatedScriptInput.setRecordPrimaryKey(3); + storeAssociatedScriptInput.setCode("var i = 2;"); + storeAssociatedScriptInput.setCommitMessage("First Commit here"); + new StoreAssociatedScriptAction().run(storeAssociatedScriptInput, storeAssociatedScriptOutput); + assertValueInField(instance, TestUtils.TABLE_NAME_PERSON_MEMORY, 3, "testScriptId", 2); + assertValueInField(instance, "script", 2, "currentScriptRevisionId", 3); + + ///////////////////////////////////// + // make sure no script on record 2 // + ///////////////////////////////////// + assertValueInField(instance, TestUtils.TABLE_NAME_PERSON_MEMORY, 2, "testScriptId", null); + + //////////////////////////////////// + // add another script to record 1 // + //////////////////////////////////// + storeAssociatedScriptInput.setRecordPrimaryKey(1); + storeAssociatedScriptInput.setCode("var i = 3;"); + storeAssociatedScriptInput.setCommitMessage("Other field"); + storeAssociatedScriptInput.setFieldName("otherScriptId"); + new StoreAssociatedScriptAction().run(storeAssociatedScriptInput, storeAssociatedScriptOutput); + assertValueInField(instance, TestUtils.TABLE_NAME_PERSON_MEMORY, 1, "testScriptId", 1); + assertValueInField(instance, TestUtils.TABLE_NAME_PERSON_MEMORY, 1, "otherScriptId", 3); + assertValueInField(instance, "script", 3, "currentScriptRevisionId", 4); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private void assertValueInField(QInstance instance, String tableName, Serializable recordId, String fieldName, Serializable value) throws QException + { + GetInput getInput = new GetInput(instance); + getInput.setSession(new QSession()); + getInput.setTableName(tableName); + getInput.setPrimaryKey(recordId); + GetOutput getOutput = new GetAction().execute(getInput); + if(getOutput.getRecord() == null) + { + fail("Expected value [" + value + "] in field [" + fieldName + "], record [" + tableName + "][" + recordId + "], but the record wasn't found..."); + } + Serializable actual = getOutput.getRecord().getValue(fieldName); + assertEquals(value, actual, "Expected value in field [" + fieldName + "], record [" + tableName + "][" + recordId + "]"); + } + +} \ No newline at end of file diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/scripts/TestScriptActionTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/scripts/TestScriptActionTest.java new file mode 100644 index 00000000..7afad2de --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/scripts/TestScriptActionTest.java @@ -0,0 +1,52 @@ +/* + * 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.actions.scripts; + + +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.scripts.TestScriptInput; +import com.kingsrook.qqq.backend.core.model.actions.scripts.TestScriptOutput; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.utils.TestUtils; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + + +/******************************************************************************* + ** Unit test for TestScriptAction + *******************************************************************************/ +class TestScriptActionTest +{ + /******************************************************************************* + ** + *******************************************************************************/ + @Test + @Disabled("Not yet done.") + void test() throws QException + { + QInstance instance = TestUtils.defineInstance(); + TestScriptInput input = new TestScriptInput(instance); + TestScriptOutput output = new TestScriptOutput(); + new TestScriptAction().run(input, output); + } + +} \ No newline at end of file diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/scripts/logging/BuildScriptLogAndScriptLogLineExecutionLoggerTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/scripts/logging/BuildScriptLogAndScriptLogLineExecutionLoggerTest.java new file mode 100644 index 00000000..dcee174b --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/scripts/logging/BuildScriptLogAndScriptLogLineExecutionLoggerTest.java @@ -0,0 +1,85 @@ +/* + * 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.actions.scripts.logging; + + +import java.util.List; +import java.util.Map; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeInput; +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.scripts.ScriptsMetaDataProvider; +import com.kingsrook.qqq.backend.core.model.session.QSession; +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.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + + +/******************************************************************************* + ** Unit test for BuildScriptLogAndScriptLogLineExecutionLogger + *******************************************************************************/ +class BuildScriptLogAndScriptLogLineExecutionLoggerTest +{ + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void test() throws QException + { + QInstance instance = TestUtils.defineInstance(); + new ScriptsMetaDataProvider().defineAll(instance, TestUtils.MEMORY_BACKEND_NAME, null); + ExecuteCodeInput executeCodeInput = new ExecuteCodeInput(instance); + executeCodeInput.setSession(new QSession()); + executeCodeInput.setInput(Map.of("a", 1)); + + BuildScriptLogAndScriptLogLineExecutionLogger logger = new BuildScriptLogAndScriptLogLineExecutionLogger(9999, 8888); + logger.acceptExecutionStart(executeCodeInput); + logger.acceptLogLine("This is a log"); + logger.acceptLogLine("This is also a log"); + logger.acceptExecutionEnd(true); + + QRecord scriptLog = logger.getScriptLog(); + assertNull(scriptLog.getValueInteger("id")); + assertNotNull(scriptLog.getValue("startTimestamp")); + assertNotNull(scriptLog.getValue("endTimestamp")); + assertNotNull(scriptLog.getValue("runTimeMillis")); + assertEquals(9999, scriptLog.getValueInteger("scriptId")); + assertEquals(8888, scriptLog.getValueInteger("scriptRevisionId")); + assertEquals("{a=1}", scriptLog.getValueString("input")); + assertEquals("true", scriptLog.getValueString("output")); + assertNull(scriptLog.getValueString("exception")); + assertFalse(scriptLog.getValueBoolean("hadError")); + + List scriptLogLineRecords = logger.getScriptLogLines(); + assertEquals(2, scriptLogLineRecords.size()); + QRecord scriptLogLine = scriptLogLineRecords.get(0); + assertNull(scriptLogLine.getValueInteger("scriptLogId")); + assertNotNull(scriptLogLine.getValue("timestamp")); + assertEquals("This is a log", scriptLogLine.getValueString("text")); + scriptLogLine = scriptLogLineRecords.get(1); + assertEquals("This is also a log", scriptLogLine.getValueString("text")); + } +} \ No newline at end of file diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/scripts/logging/StoreScriptLogAndScriptLogLineExecutionLoggerTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/scripts/logging/StoreScriptLogAndScriptLogLineExecutionLoggerTest.java new file mode 100644 index 00000000..42fb6cca --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/scripts/logging/StoreScriptLogAndScriptLogLineExecutionLoggerTest.java @@ -0,0 +1,103 @@ +/* + * 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.actions.scripts.logging; + + +import java.util.List; +import java.util.Map; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeInput; +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.scripts.ScriptsMetaDataProvider; +import com.kingsrook.qqq.backend.core.model.session.QSession; +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; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +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; + + +/******************************************************************************* + ** Unit test for StoreScriptLogAndScriptLogLineExecutionLogger + *******************************************************************************/ +class StoreScriptLogAndScriptLogLineExecutionLoggerTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @BeforeEach + @AfterEach + void beforeAndAfterEach() + { + MemoryRecordStore.getInstance().reset(); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void test() throws QException + { + QInstance instance = TestUtils.defineInstance(); + new ScriptsMetaDataProvider().defineAll(instance, TestUtils.MEMORY_BACKEND_NAME, null); + ExecuteCodeInput executeCodeInput = new ExecuteCodeInput(instance); + executeCodeInput.setSession(new QSession()); + executeCodeInput.setInput(Map.of("a", 1)); + + StoreScriptLogAndScriptLogLineExecutionLogger logger = new StoreScriptLogAndScriptLogLineExecutionLogger(9999, 8888); + logger.acceptExecutionStart(executeCodeInput); + logger.acceptLogLine("This is a log"); + logger.acceptLogLine("This is also a log"); + logger.acceptExecutionEnd(true); + + List scriptLogRecords = TestUtils.queryTable(instance, "scriptLog"); + assertEquals(1, scriptLogRecords.size()); + QRecord scriptLog = scriptLogRecords.get(0); + assertNotNull(scriptLog.getValueInteger("id")); + assertNotNull(scriptLog.getValue("startTimestamp")); + assertNotNull(scriptLog.getValue("endTimestamp")); + assertNotNull(scriptLog.getValue("runTimeMillis")); + assertEquals(9999, scriptLog.getValueInteger("scriptId")); + assertEquals(8888, scriptLog.getValueInteger("scriptRevisionId")); + assertEquals("{a=1}", scriptLog.getValueString("input")); + assertEquals("true", scriptLog.getValueString("output")); + assertNull(scriptLog.getValueString("exception")); + assertFalse(scriptLog.getValueBoolean("hadError")); + + List scriptLogLineRecords = TestUtils.queryTable(instance, "scriptLogLine"); + assertEquals(2, scriptLogLineRecords.size()); + QRecord scriptLogLine = scriptLogLineRecords.get(0); + assertEquals(scriptLog.getValueInteger("id"), scriptLogLine.getValueInteger("scriptLogId")); + assertNotNull(scriptLogLine.getValue("timestamp")); + assertEquals("This is a log", scriptLogLine.getValueString("text")); + scriptLogLine = scriptLogLineRecords.get(1); + assertEquals("This is also a log", scriptLogLine.getValueString("text")); + } +} \ No newline at end of file diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/GetActionTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/GetActionTest.java new file mode 100644 index 00000000..dd576194 --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/GetActionTest.java @@ -0,0 +1,58 @@ +/* + * 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.actions.tables; + + +import com.kingsrook.qqq.backend.core.exceptions.QException; +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.TestUtils; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertNotNull; + + +/******************************************************************************* + ** Unit test for GetAction + ** + *******************************************************************************/ +class GetActionTest +{ + + /******************************************************************************* + ** At the core level, there isn't much that can be asserted, as it uses the + ** mock implementation - just confirming that all of the "wiring" works. + ** + *******************************************************************************/ + @Test + public void test() throws QException + { + GetInput request = new GetInput(TestUtils.defineInstance()); + request.setSession(TestUtils.getMockSession()); + request.setTableName("person"); + request.setPrimaryKey(1); + request.setShouldGenerateDisplayValues(true); + request.setShouldTranslatePossibleValues(true); + GetOutput result = new GetAction().execute(request); + assertNotNull(result); + assertNotNull(result.getRecord()); + } +} diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java index d6da4b05..2d20dbc6 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java @@ -51,8 +51,10 @@ import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppSection; import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon; 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.queues.SQSQueueProviderMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportDataSource; import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier; @@ -142,9 +144,8 @@ class QInstanceValidatorTest qInstance.setTables(null); qInstance.setProcesses(null); }, - "At least 1 table must be defined", - "Unrecognized table shape for possibleValueSource shape", - "Unrecognized processName for queue"); + true, + "At least 1 table must be defined"); } @@ -161,9 +162,8 @@ class QInstanceValidatorTest qInstance.setTables(new HashMap<>()); qInstance.setProcesses(new HashMap<>()); }, - "At least 1 table must be defined", - "Unrecognized table shape for possibleValueSource shape", - "Unrecognized processName for queue"); + true, + "At least 1 table must be defined"); } @@ -569,6 +569,40 @@ class QInstanceValidatorTest + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testFieldSectionDuplicateName() + { + QTableMetaData table = new QTableMetaData().withName("test") + .withBackendName(TestUtils.DEFAULT_BACKEND_NAME) + .withSection(new QFieldSection("section1", "Section 1", new QIcon("person"), Tier.T1, List.of("id"))) + .withSection(new QFieldSection("section1", "Section 2", new QIcon("person"), Tier.T2, List.of("name"))) + .withField(new QFieldMetaData("id", QFieldType.INTEGER)) + .withField(new QFieldMetaData("name", QFieldType.INTEGER)); + assertValidationFailureReasons((qInstance) -> qInstance.addTable(table), "more than 1 section named"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testFieldSectionDuplicateLabel() + { + QTableMetaData table = new QTableMetaData().withName("test") + .withBackendName(TestUtils.DEFAULT_BACKEND_NAME) + .withSection(new QFieldSection("section1", "Section 1", new QIcon("person"), Tier.T1, List.of("id"))) + .withSection(new QFieldSection("section2", "Section 1", new QIcon("person"), Tier.T2, List.of("name"))) + .withField(new QFieldMetaData("id", QFieldType.INTEGER)) + .withField(new QFieldMetaData("name", QFieldType.INTEGER)); + assertValidationFailureReasons((qInstance) -> qInstance.addTable(table), "more than 1 section labeled"); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -680,10 +714,13 @@ class QInstanceValidatorTest @Test void testAppSectionsMissingLabel() { + /////////////////////////////////////////////////////////////////////////////////// + // the enricher makes a label from the name, so, we'll just make them both null. // + /////////////////////////////////////////////////////////////////////////////////// QAppMetaData app = new QAppMetaData().withName("test") .withChild(new QTableMetaData().withName("test")) - .withSection(new QAppSection("Section 1", null, new QIcon("person"), List.of("test"), null, null)); - assertValidationFailureReasons((qInstance) -> qInstance.addApp(app), "Missing a label"); + .withSection(new QAppSection(null, null, new QIcon("person"), List.of("test"), null, null)); + assertValidationFailureReasons((qInstance) -> qInstance.addApp(app), "Missing a label", "Missing a name"); } @@ -802,6 +839,7 @@ class QInstanceValidatorTest possibleValueSource.setOrderByFields(List.of(new QFilterOrderBy("id"))); possibleValueSource.setCustomCodeReference(new QCodeReference()); possibleValueSource.setEnumValues(null); + possibleValueSource.setType(QPossibleValueSourceType.ENUM); }, "should not have a tableName", "should not have searchFields", @@ -828,6 +866,7 @@ class QInstanceValidatorTest possibleValueSource.setOrderByFields(new ArrayList<>()); possibleValueSource.setCustomCodeReference(new QCodeReference()); possibleValueSource.setEnumValues(List.of(new QPossibleValue<>("test"))); + possibleValueSource.setType(QPossibleValueSourceType.TABLE); }, "should not have enum values", "should not have a customCodeReference", @@ -857,6 +896,7 @@ class QInstanceValidatorTest possibleValueSource.setOrderByFields(List.of(new QFilterOrderBy("id"))); possibleValueSource.setCustomCodeReference(null); possibleValueSource.setEnumValues(List.of(new QPossibleValue<>("test"))); + possibleValueSource.setType(QPossibleValueSourceType.CUSTOM); }, "should not have enum values", "should not have a tableName", @@ -1251,6 +1291,138 @@ class QInstanceValidatorTest + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testReportName() + { + assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).withName(null), + "Inconsistent naming for report"); + + assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).withName(""), + "Inconsistent naming for report"); + + assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).withName("wrongName"), + "Inconsistent naming for report"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testReportNoDataSources() + { + assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).withDataSources(null), + "At least 1 data source", + "unrecognized dataSourceName"); + + assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).withDataSources(new ArrayList<>()), + "At least 1 data source", + "unrecognized dataSourceName"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testReportDataSourceNames() + { + assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getDataSources().get(0).setName(null), + "Missing name for a dataSource", + "unrecognized dataSourceName"); + + assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getDataSources().get(0).setName(""), + "Missing name for a dataSource", + "unrecognized dataSourceName"); + + assertValidationFailureReasons((qInstance) -> + { + List dataSources = new ArrayList<>(qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getDataSources()); + dataSources.add(dataSources.get(0)); + qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).setDataSources(dataSources); + }, + "More than one dataSource with name"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testReportDataSourceTables() + { + assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getDataSources().get(0).setSourceTable("notATable"), + "is not a table in this instance"); + + assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getDataSources().get(0).setSourceTable(null), + "does not have a sourceTable"); + + assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getDataSources().get(0).setSourceTable(""), + "does not have a sourceTable"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testReportDataSourceTablesFilter() + { + assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getDataSources().get(0).getQueryFilter().getCriteria().get(0).setFieldName(null), + "Missing fieldName for a criteria"); + + assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getDataSources().get(0).getQueryFilter().getCriteria().get(0).setFieldName("notAField"), + "is not a field in this table"); + + assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getDataSources().get(0).getQueryFilter().getCriteria().get(0).setOperator(null), + "Missing operator for a criteria"); + + assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getDataSources().get(0).getQueryFilter().withOrderBy(new QFilterOrderBy(null)), + "Missing fieldName for an orderBy"); + + assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getDataSources().get(0).getQueryFilter().withOrderBy(new QFilterOrderBy("notAField")), + "is not a field in this table"); + + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testReportDataSourceStaticDataSupplier() + { + assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getDataSources().get(0).withStaticDataSupplier(new QCodeReference()), + "has both a sourceTable and a staticDataSupplier"); + + assertValidationFailureReasons((qInstance) -> + { + QReportDataSource dataSource = qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getDataSources().get(0); + dataSource.setSourceTable(null); + dataSource.setStaticDataSupplier(new QCodeReference(null, QCodeType.JAVA, null)); + }, + "missing a code reference name"); + + assertValidationFailureReasons((qInstance) -> + { + QReportDataSource dataSource = qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getDataSources().get(0); + dataSource.setSourceTable(null); + dataSource.setStaticDataSupplier(new QCodeReference(ArrayList.class, null)); + }, + "is not of the expected type"); + + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/enumeration/EnumerationQueryActionTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/enumeration/EnumerationQueryActionTest.java new file mode 100644 index 00000000..24d7d013 --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/enumeration/EnumerationQueryActionTest.java @@ -0,0 +1,271 @@ +/* + * 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.modules.backend.implementations.enumeration; + + +import java.util.List; +import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; +import com.kingsrook.qqq.backend.core.exceptions.QException; +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.data.QRecordEnum; +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.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.model.session.QSession; +import com.kingsrook.qqq.backend.core.utils.TestUtils; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; + + +/******************************************************************************* + ** Unit test for EnumerationQueryAction + *******************************************************************************/ +class EnumerationQueryActionTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testUnfilteredQuery() throws QException + { + QInstance instance = defineQInstance(); + + QueryInput queryInput = new QueryInput(instance); + queryInput.setSession(new QSession()); + queryInput.setTableName("statesEnum"); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + assertEquals(2, queryOutput.getRecords().size()); + + assertEquals(1, queryOutput.getRecords().get(0).getValueInteger("id")); + assertEquals("Missouri", queryOutput.getRecords().get(0).getValueString("name")); + assertEquals("MO", queryOutput.getRecords().get(0).getValueString("postalCode")); + assertEquals(15_000_000, queryOutput.getRecords().get(0).getValueInteger("population")); + + assertEquals(2, queryOutput.getRecords().get(1).getValueInteger("id")); + assertEquals("Illinois", queryOutput.getRecords().get(1).getValueString("name")); + assertEquals("IL", queryOutput.getRecords().get(1).getValueString("postalCode")); + assertEquals(25_000_000, queryOutput.getRecords().get(1).getValueInteger("population")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testFilteredQuery() throws QException + { + QInstance instance = defineQInstance(); + + QueryInput queryInput = new QueryInput(instance); + queryInput.setSession(new QSession()); + queryInput.setTableName("statesEnum"); + queryInput.setFilter(new QQueryFilter().withCriteria(new QFilterCriteria("population", QCriteriaOperator.GREATER_THAN, List.of(20_000_000)))); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + assertEquals(1, queryOutput.getRecords().size()); + + assertEquals(2, queryOutput.getRecords().get(0).getValueInteger("id")); + assertEquals("IL", queryOutput.getRecords().get(0).getValueString("postalCode")); + assertEquals(25_000_000, queryOutput.getRecords().get(0).getValueInteger("population")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testQueryOrderBy() throws QException + { + QInstance instance = defineQInstance(); + + QueryInput queryInput = new QueryInput(instance); + queryInput.setSession(new QSession()); + queryInput.setTableName("statesEnum"); + + queryInput.setFilter(new QQueryFilter().withOrderBy(new QFilterOrderBy("name"))); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + assertEquals(List.of("Illinois", "Missouri"), queryOutput.getRecords().stream().map(r -> r.getValueString("name")).toList()); + + queryInput.setFilter(new QQueryFilter().withOrderBy(new QFilterOrderBy("name", false))); + queryOutput = new QueryAction().execute(queryInput); + assertEquals(List.of("Missouri", "Illinois"), queryOutput.getRecords().stream().map(r -> r.getValueString("name")).toList()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testQuerySkipLimit() throws QException + { + QInstance instance = defineQInstance(); + + QueryInput queryInput = new QueryInput(instance); + queryInput.setSession(new QSession()); + queryInput.setTableName("statesEnum"); + queryInput.setSkip(0); + queryInput.setLimit(null); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + assertEquals(List.of("Missouri", "Illinois"), queryOutput.getRecords().stream().map(r -> r.getValueString("name")).toList()); + + queryInput.setSkip(1); + queryInput.setLimit(null); + queryOutput = new QueryAction().execute(queryInput); + assertEquals(List.of("Illinois"), queryOutput.getRecords().stream().map(r -> r.getValueString("name")).toList()); + + queryInput.setSkip(2); + queryInput.setLimit(null); + queryOutput = new QueryAction().execute(queryInput); + assertEquals(List.of(), queryOutput.getRecords().stream().map(r -> r.getValueString("name")).toList()); + + queryInput.setSkip(null); + queryInput.setLimit(1); + queryOutput = new QueryAction().execute(queryInput); + assertEquals(List.of("Missouri"), queryOutput.getRecords().stream().map(r -> r.getValueString("name")).toList()); + + queryInput.setSkip(null); + queryInput.setLimit(2); + queryOutput = new QueryAction().execute(queryInput); + assertEquals(List.of("Missouri", "Illinois"), queryOutput.getRecords().stream().map(r -> r.getValueString("name")).toList()); + + queryInput.setSkip(null); + queryInput.setLimit(3); + queryOutput = new QueryAction().execute(queryInput); + assertEquals(List.of("Missouri", "Illinois"), queryOutput.getRecords().stream().map(r -> r.getValueString("name")).toList()); + + queryInput.setSkip(null); + queryInput.setLimit(0); + queryOutput = new QueryAction().execute(queryInput); + assertEquals(List.of(), queryOutput.getRecords().stream().map(r -> r.getValueString("name")).toList()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private QInstance defineQInstance() + { + QInstance instance = TestUtils.defineInstance(); + instance.addBackend(new QBackendMetaData() + .withName("enum") + .withBackendType("enum") + ); + + instance.addTable(new QTableMetaData() + .withName("statesEnum") + .withBackendName("enum") + .withPrimaryKeyField("id") + .withField(new QFieldMetaData("id", QFieldType.INTEGER)) + .withField(new QFieldMetaData("name", QFieldType.STRING)) + .withField(new QFieldMetaData("postalCode", QFieldType.STRING)) + .withField(new QFieldMetaData("population", QFieldType.INTEGER)) + .withBackendDetails(new EnumerationTableBackendDetails().withEnumClass(States.class)) + ); + return instance; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static enum States implements QRecordEnum + { + MO(1, "Missouri", "MO", 15_000_000), + IL(2, "Illinois", "IL", 25_000_000); + + + private final Integer id; + private final String name; + private final String postalCode; + private final Integer population; + + + + /******************************************************************************* + ** + *******************************************************************************/ + States(int id, String name, String postalCode, int population) + { + this.id = id; + this.name = name; + this.postalCode = postalCode; + this.population = population; + } + + + + /******************************************************************************* + ** Getter for id + ** + *******************************************************************************/ + public Integer getId() + { + return id; + } + + + + /******************************************************************************* + ** Getter for name + ** + *******************************************************************************/ + public String getName() + { + return name; + } + + + + /******************************************************************************* + ** Getter for postalCode + ** + *******************************************************************************/ + public String getPostalCode() + { + return postalCode; + } + + + + /******************************************************************************* + ** Getter for population + ** + *******************************************************************************/ + public Integer getPopulation() + { + return population; + } + } + +} \ No newline at end of file diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryBackendModuleTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryBackendModuleTest.java index 3f451a6b..21e00c65 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryBackendModuleTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryBackendModuleTest.java @@ -271,12 +271,12 @@ class MemoryBackendModuleTest assertEquals(2, queryShapes(qInstance, table, session, new QFilterCriteria("id", QCriteriaOperator.LESS_THAN_OR_EQUALS, List.of(2))).size()); assertEquals(1, queryShapes(qInstance, table, session, new QFilterCriteria("id", QCriteriaOperator.LESS_THAN_OR_EQUALS, List.of(1))).size()); assertEquals(0, queryShapes(qInstance, table, session, new QFilterCriteria("id", QCriteriaOperator.LESS_THAN_OR_EQUALS, List.of(0))).size()); + assertEquals(1, queryShapes(qInstance, table, session, new QFilterCriteria("name", QCriteriaOperator.LESS_THAN_OR_EQUALS, List.of("Darin"))).size()); assertThrows(QException.class, () -> queryShapes(qInstance, table, session, new QFilterCriteria("id", QCriteriaOperator.GREATER_THAN, List.of()))); assertThrows(QException.class, () -> queryShapes(qInstance, table, session, new QFilterCriteria("id", QCriteriaOperator.GREATER_THAN_OR_EQUALS, List.of()))); assertThrows(QException.class, () -> queryShapes(qInstance, table, session, new QFilterCriteria("id", QCriteriaOperator.LESS_THAN, List.of()))); assertThrows(QException.class, () -> queryShapes(qInstance, table, session, new QFilterCriteria("id", QCriteriaOperator.LESS_THAN_OR_EQUALS, List.of()))); - assertThrows(QException.class, () -> queryShapes(qInstance, table, session, new QFilterCriteria("name", QCriteriaOperator.LESS_THAN_OR_EQUALS, List.of("Bob")))); { //////////////////////////// diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/basepull/ExtractViaBasepullQueryStepTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/basepull/ExtractViaBasepullQueryStepTest.java new file mode 100644 index 00000000..096ebae1 --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/basepull/ExtractViaBasepullQueryStepTest.java @@ -0,0 +1,74 @@ +/* + * 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.processes.implementations.basepull; + + +import java.time.Instant; +import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction; +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.tables.query.QCriteriaOperator; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; +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.assertTrue; + + +/******************************************************************************* + ** Unit test for ExtractViaBasepullQueryStep + *******************************************************************************/ +class ExtractViaBasepullQueryStepTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void test() throws QException + { + Instant timestamp = Instant.parse("1980-05-31T15:36:00Z"); + Instant now = Instant.now(); + + RunBackendStepInput input = new RunBackendStepInput(TestUtils.defineInstance()); + input.addValue(RunProcessAction.BASEPULL_TIMESTAMP_FIELD, "createDate"); + input.addValue(RunProcessAction.BASEPULL_THIS_RUNTIME_KEY, now); + input.setBasepullLastRunTime(timestamp); + QQueryFilter queryFilter = new ExtractViaBasepullQueryStep().getQueryFilter(input); + + System.out.println(queryFilter); + + assertEquals(2, queryFilter.getCriteria().size()); + assertEquals("createDate", queryFilter.getCriteria().get(0).getFieldName()); + assertEquals(QCriteriaOperator.GREATER_THAN, queryFilter.getCriteria().get(0).getOperator()); + assertEquals(timestamp.toString(), queryFilter.getCriteria().get(0).getValues().get(0)); + + assertEquals("createDate", queryFilter.getCriteria().get(1).getFieldName()); + assertEquals(QCriteriaOperator.LESS_THAN_OR_EQUALS, queryFilter.getCriteria().get(1).getOperator()); + assertEquals(now.toString(), queryFilter.getCriteria().get(1).getValues().get(0)); + + assertEquals(1, queryFilter.getOrderBys().size()); + assertEquals("createDate", queryFilter.getOrderBys().get(0).getFieldName()); + assertTrue(queryFilter.getOrderBys().get(0).getIsAscending()); + } + +} diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/RunReportForRecordProcessTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/RunReportForRecordProcessTest.java new file mode 100644 index 00000000..57ae10cf --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/RunReportForRecordProcessTest.java @@ -0,0 +1,62 @@ +/* + * 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.processes.implementations.reports; + + +import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction; +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.metadata.QInstance; +import com.kingsrook.qqq.backend.core.utils.TestUtils; +import org.junit.jupiter.api.Test; + + +/******************************************************************************* + ** Unit test for BasicRunReportProcess + *******************************************************************************/ +class RunReportForRecordProcessTest +{ + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testRunReport() throws QException + { + QInstance instance = TestUtils.defineInstance(); + TestUtils.insertDefaultShapes(instance); + + RunProcessInput runProcessInput = new RunProcessInput(instance); + runProcessInput.setSession(TestUtils.getMockSession()); + runProcessInput.setProcessName(TestUtils.PROCESS_NAME_RUN_SHAPES_PERSON_REPORT); + runProcessInput.addValue(BasicRunReportProcess.FIELD_REPORT_NAME, TestUtils.REPORT_NAME_SHAPES_PERSON); + runProcessInput.addValue("recordsParam", "recordIds"); + runProcessInput.addValue("recordIds", "1"); + + RunProcessOutput runProcessOutput = new RunProcessAction().execute(runProcessInput); + + // runProcessOutput = new RunProcessAction().execute(runProcessInput); + // assertThat(runProcessOutput.getProcessState().getNextStepName()).isPresent().get().isEqualTo(BasicRunReportProcess.STEP_NAME_ACCESS); + // assertThat(runProcessOutput.getValues()).containsKeys("downloadFileName", "serverFilePath"); + } + +} \ No newline at end of file diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/utils/GeneralProcessUtilsTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/utils/GeneralProcessUtilsTest.java new file mode 100644 index 00000000..ad98187a --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/utils/GeneralProcessUtilsTest.java @@ -0,0 +1,326 @@ +/* + * 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.processes.utils; + + +import java.io.Serializable; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; +import com.kingsrook.qqq.backend.core.exceptions.QException; +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.data.QRecord; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.session.QSession; +import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryRecordStore; +import com.kingsrook.qqq.backend.core.utils.ListingHash; +import com.kingsrook.qqq.backend.core.utils.TestUtils; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + + +/******************************************************************************* + ** Unit test for GeneralProcessUtils + *******************************************************************************/ +class GeneralProcessUtilsTest +{ + /******************************************************************************* + ** + *******************************************************************************/ + @AfterEach + @BeforeEach + void beforeAndAfterEach() + { + MemoryRecordStore.getInstance().reset(); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testGetForeignRecordMap() throws QException + { + QInstance instance = TestUtils.defineInstance(); + + TestUtils.insertRecords(instance, instance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY), List.of( + new QRecord().withValue("favoriteShapeId", 3), + new QRecord().withValue("favoriteShapeId", 1) + )); + TestUtils.insertDefaultShapes(instance); + + QueryInput queryInput = new QueryInput(instance); + queryInput.setSession(new QSession()); + queryInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + + Map foreignRecordMap = GeneralProcessUtils.getForeignRecordMap(queryInput, queryOutput.getRecords(), "favoriteShapeId", TestUtils.TABLE_NAME_SHAPE, "id"); + + assertEquals(2, foreignRecordMap.size()); + assertEquals(1, foreignRecordMap.get(1).getValueInteger("id")); + assertEquals(3, foreignRecordMap.get(3).getValueInteger("id")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testGetForeignRecordListingHashMap() throws QException + { + QInstance instance = TestUtils.defineInstance(); + + TestUtils.insertRecords(instance, instance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY), List.of( + new QRecord().withValue("id", 1).withValue("favoriteShapeId", 3), + new QRecord().withValue("id", 2).withValue("favoriteShapeId", 3), + new QRecord().withValue("id", 3).withValue("favoriteShapeId", 1) + )); + TestUtils.insertDefaultShapes(instance); + + QueryInput queryInput = new QueryInput(instance); + queryInput.setSession(new QSession()); + queryInput.setTableName(TestUtils.TABLE_NAME_SHAPE); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + + ListingHash foreignRecordListingHashMap = GeneralProcessUtils.getForeignRecordListingHashMap(queryInput, queryOutput.getRecords(), "id", TestUtils.TABLE_NAME_PERSON_MEMORY, "favoriteShapeId"); + + assertEquals(2, foreignRecordListingHashMap.size()); + + assertEquals(1, foreignRecordListingHashMap.get(1).size()); + assertEquals(Set.of(3), foreignRecordListingHashMap.get(1).stream().map(r -> r.getValueInteger("id")).collect(Collectors.toSet())); + + assertEquals(2, foreignRecordListingHashMap.get(3).size()); + assertEquals(Set.of(1, 2), foreignRecordListingHashMap.get(3).stream().map(r -> r.getValueInteger("id")).collect(Collectors.toSet())); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testAddForeignRecordsToRecordList() throws QException + { + QInstance instance = TestUtils.defineInstance(); + + TestUtils.insertRecords(instance, instance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY), List.of( + new QRecord().withValue("favoriteShapeId", 3), + new QRecord().withValue("favoriteShapeId", 1) + )); + TestUtils.insertDefaultShapes(instance); + + QueryInput queryInput = new QueryInput(instance); + queryInput.setSession(new QSession()); + queryInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + + GeneralProcessUtils.addForeignRecordsToRecordList(queryInput, queryOutput.getRecords(), "favoriteShapeId", TestUtils.TABLE_NAME_SHAPE, "id"); + + for(QRecord record : queryOutput.getRecords()) + { + assertEquals(record.getValue("favoriteShapeId"), ((QRecord) record.getValue(TestUtils.TABLE_NAME_SHAPE)).getValue("id")); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testAddForeignRecordsListToRecordList() throws QException + { + QInstance instance = TestUtils.defineInstance(); + + TestUtils.insertRecords(instance, instance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY), List.of( + new QRecord().withValue("id", 1).withValue("favoriteShapeId", 3), + new QRecord().withValue("id", 2).withValue("favoriteShapeId", 3), + new QRecord().withValue("id", 3).withValue("favoriteShapeId", 1) + )); + TestUtils.insertDefaultShapes(instance); + + QueryInput queryInput = new QueryInput(instance); + queryInput.setSession(new QSession()); + queryInput.setTableName(TestUtils.TABLE_NAME_SHAPE); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + + GeneralProcessUtils.addForeignRecordsListToRecordList(queryInput, queryOutput.getRecords(), "id", TestUtils.TABLE_NAME_PERSON_MEMORY, "favoriteShapeId"); + + for(QRecord record : queryOutput.getRecords()) + { + @SuppressWarnings("unchecked") + List foreignRecordList = (List) record.getValue(TestUtils.TABLE_NAME_PERSON_MEMORY); + + if(record.getValueInteger("id").equals(3)) + { + assertEquals(2, foreignRecordList.size()); + } + else if(record.getValueInteger("id").equals(2)) + { + assertNull(foreignRecordList); + continue; + } + + for(QRecord foreignRecord : foreignRecordList) + { + assertEquals(record.getValue("id"), foreignRecord.getValue("favoriteShapeId")); + } + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testGetRecordListByField() throws QException + { + QInstance instance = TestUtils.defineInstance(); + + TestUtils.insertRecords(instance, instance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY), List.of( + new QRecord().withValue("id", 1).withValue("favoriteShapeId", 3), + new QRecord().withValue("id", 2).withValue("favoriteShapeId", 3), + new QRecord().withValue("id", 3).withValue("favoriteShapeId", 1) + )); + + QueryInput queryInput = new QueryInput(instance); + queryInput.setSession(new QSession()); + List records = GeneralProcessUtils.getRecordListByField(queryInput, TestUtils.TABLE_NAME_PERSON_MEMORY, "favoriteShapeId", 3); + assertEquals(2, records.size()); + assertTrue(records.stream().anyMatch(r -> r.getValue("id").equals(1))); + assertTrue(records.stream().anyMatch(r -> r.getValue("id").equals(2))); + assertTrue(records.stream().noneMatch(r -> r.getValue("id").equals(3))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testGetRecordById() throws QException + { + QInstance instance = TestUtils.defineInstance(); + + TestUtils.insertRecords(instance, instance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY), List.of( + new QRecord().withValue("id", 1).withValue("firstName", "Darin"), + new QRecord().withValue("id", 2).withValue("firstName", "James"), + new QRecord().withValue("id", 3).withValue("firstName", "Tim") + )); + + QueryInput queryInput = new QueryInput(instance); + queryInput.setSession(new QSession()); + Optional record = GeneralProcessUtils.getRecordById(queryInput, TestUtils.TABLE_NAME_PERSON_MEMORY, "firstName", "James"); + assertTrue(record.isPresent()); + assertEquals(2, record.get().getValueInteger("id")); + + record = GeneralProcessUtils.getRecordById(queryInput, TestUtils.TABLE_NAME_PERSON_MEMORY, "firstName", "Bobby"); + assertFalse(record.isPresent()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testLoadTable() throws QException + { + QInstance instance = TestUtils.defineInstance(); + + TestUtils.insertRecords(instance, instance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY), List.of( + new QRecord().withValue("id", 1).withValue("firstName", "Darin"), + new QRecord().withValue("id", 2).withValue("firstName", "James"), + new QRecord().withValue("id", 3).withValue("firstName", "Tim") + )); + + QueryInput queryInput = new QueryInput(instance); + queryInput.setSession(new QSession()); + List records = GeneralProcessUtils.loadTable(queryInput, TestUtils.TABLE_NAME_PERSON_MEMORY); + assertEquals(3, records.size()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testLoadTableToMap() throws QException + { + QInstance instance = TestUtils.defineInstance(); + + TestUtils.insertRecords(instance, instance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY), List.of( + new QRecord().withValue("id", 1).withValue("firstName", "Darin"), + new QRecord().withValue("id", 2).withValue("firstName", "James"), + new QRecord().withValue("id", 3).withValue("firstName", "Tim") + )); + + QueryInput queryInput = new QueryInput(instance); + queryInput.setSession(new QSession()); + Map recordMapById = GeneralProcessUtils.loadTableToMap(queryInput, TestUtils.TABLE_NAME_PERSON_MEMORY, "id"); + assertEquals(3, recordMapById.size()); + assertEquals("Darin", recordMapById.get(1).getValueString("firstName")); + assertEquals("James", recordMapById.get(2).getValueString("firstName")); + + Map recordMapByFirstName = GeneralProcessUtils.loadTableToMap(queryInput, TestUtils.TABLE_NAME_PERSON_MEMORY, "firstName"); + assertEquals(3, recordMapByFirstName.size()); + assertEquals(1, recordMapByFirstName.get("Darin").getValueInteger("id")); + assertEquals(3, recordMapByFirstName.get("Tim").getValueInteger("id")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testLoadTableToListingHash() throws QException + { + QInstance instance = TestUtils.defineInstance(); + + TestUtils.insertRecords(instance, instance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY), List.of( + new QRecord().withValue("id", 1).withValue("firstName", "Darin").withValue("lastName", "Kelkhoff"), + new QRecord().withValue("id", 2).withValue("firstName", "James").withValue("lastName", "Maes"), + new QRecord().withValue("id", 3).withValue("firstName", "James").withValue("lastName", "Brown") + )); + + QueryInput queryInput = new QueryInput(instance); + queryInput.setSession(new QSession()); + ListingHash map = GeneralProcessUtils.loadTableToListingHash(queryInput, TestUtils.TABLE_NAME_PERSON_MEMORY, "firstName"); + assertEquals(2, map.size()); + assertEquals(1, map.get("Darin").size()); + assertEquals(2, map.get("James").size()); + } +} \ No newline at end of file diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java index 618da13e..b22f1f00 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java @@ -76,6 +76,11 @@ import com.kingsrook.qqq.backend.core.model.metadata.processes.QRecordListMetaDa import com.kingsrook.qqq.backend.core.model.metadata.queues.QQueueMetaData; import com.kingsrook.qqq.backend.core.model.metadata.queues.QQueueProviderMetaData; 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; +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.reporting.ReportType; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.AutomationStatusTracking; import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.AutomationStatusTrackingType; @@ -87,9 +92,11 @@ import com.kingsrook.qqq.backend.core.modules.authentication.MockAuthenticationM import com.kingsrook.qqq.backend.core.modules.authentication.metadata.QAuthenticationMetaData; import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryBackendModule; import com.kingsrook.qqq.backend.core.modules.backend.implementations.mock.MockBackendModule; +import com.kingsrook.qqq.backend.core.processes.implementations.basepull.BasepullConfiguration; import com.kingsrook.qqq.backend.core.processes.implementations.etl.basic.BasicETLProcess; import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamed.StreamedETLProcess; import com.kingsrook.qqq.backend.core.processes.implementations.mock.MockBackendStep; +import com.kingsrook.qqq.backend.core.processes.implementations.reports.RunReportForRecordProcess; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -116,9 +123,13 @@ public class TestUtils public static final String PROCESS_NAME_GREET_PEOPLE_INTERACTIVE = "greetInteractive"; public static final String PROCESS_NAME_INCREASE_BIRTHDATE = "increaseBirthdate"; public static final String PROCESS_NAME_ADD_TO_PEOPLES_AGE = "addToPeoplesAge"; + public static final String PROCESS_NAME_BASEPULL = "basepullTest"; + public static final String PROCESS_NAME_RUN_SHAPES_PERSON_REPORT = "runShapesPersonReport"; public static final String TABLE_NAME_PERSON_FILE = "personFile"; public static final String TABLE_NAME_PERSON_MEMORY = "personMemory"; public static final String TABLE_NAME_ID_AND_NAME_ONLY = "idAndNameOnly"; + public static final String TABLE_NAME_BASEPULL = "basepullTest"; + public static final String REPORT_NAME_SHAPES_PERSON = "shapesPersonReport"; public static final String POSSIBLE_VALUE_SOURCE_STATE = "state"; // enum-type public static final String POSSIBLE_VALUE_SOURCE_SHAPE = "shape"; // table-type @@ -128,6 +139,9 @@ public class TestUtils public static final String POLLING_AUTOMATION = "polling"; public static final String DEFAULT_QUEUE_PROVIDER = "defaultQueueProvider"; + public static final String BASEPULL_KEY_FIELD_NAME = "processName"; + public static final String BASEPULL_LAST_RUN_TIME_FIELD_NAME = "lastRunTime"; + /******************************************************************************* @@ -146,6 +160,7 @@ public class TestUtils qInstance.addTable(definePersonMemoryTable()); qInstance.addTable(defineTableIdAndNameOnly()); qInstance.addTable(defineTableShape()); + qInstance.addTable(defineTableBasepull()); qInstance.addPossibleValueSource(defineAutomationStatusPossibleValueSource()); qInstance.addPossibleValueSource(defineStatesPossibleValueSource()); @@ -158,6 +173,10 @@ public class TestUtils qInstance.addProcess(new BasicETLProcess().defineProcessMetaData()); qInstance.addProcess(new StreamedETLProcess().defineProcessMetaData()); qInstance.addProcess(defineProcessIncreasePersonBirthdate()); + qInstance.addProcess(defineProcessBasepull()); + + qInstance.addReport(defineShapesPersonsReport()); + qInstance.addProcess(defineShapesPersonReportProcess()); qInstance.addAutomationProvider(definePollingAutomationProvider()); @@ -486,6 +505,26 @@ public class TestUtils + /******************************************************************************* + ** Define a basepullTable + *******************************************************************************/ + public static QTableMetaData defineTableBasepull() + { + return (new QTableMetaData() + .withName(TABLE_NAME_BASEPULL) + .withLabel("Basepull Test") + .withPrimaryKeyField("id") + .withBackendName(MEMORY_BACKEND_NAME) + .withFields(TestUtils.defineTablePerson().getFields())) + .withField(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false)) + .withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME).withBackendName("create_date").withIsEditable(false)) + .withField(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME).withBackendName("modify_date").withIsEditable(false)) + .withField(new QFieldMetaData(BASEPULL_KEY_FIELD_NAME, QFieldType.STRING).withBackendName("process_name").withIsRequired(true)) + .withField(new QFieldMetaData(BASEPULL_LAST_RUN_TIME_FIELD_NAME, QFieldType.DATE_TIME).withBackendName("last_run_time").withIsRequired(true)); + } + + + /******************************************************************************* ** Define a 3nd version of the 'person' table, backed by the in-memory backend *******************************************************************************/ @@ -732,6 +771,43 @@ public class TestUtils + /******************************************************************************* + ** Define a sample basepull process + *******************************************************************************/ + private static QProcessMetaData defineProcessBasepull() + { + return new QProcessMetaData() + .withBasepullConfiguration(new BasepullConfiguration() + .withKeyField(BASEPULL_KEY_FIELD_NAME) + .withLastRunTimeFieldName(BASEPULL_LAST_RUN_TIME_FIELD_NAME) + .withHoursBackForInitialTimestamp(24) + .withKeyValue(PROCESS_NAME_BASEPULL) + .withTableName(defineTableBasepull().getName())) + .withName(PROCESS_NAME_BASEPULL) + .withTableName(TABLE_NAME_PERSON) + .addStep(new QBackendStepMetaData() + .withName("prepare") + .withCode(new QCodeReference() + .withName(MockBackendStep.class.getName()) + .withCodeType(QCodeType.JAVA) + .withCodeUsage(QCodeUsage.BACKEND_STEP)) // todo - needed, or implied in this context? + .withInputData(new QFunctionInputMetaData() + .withRecordListMetaData(new QRecordListMetaData().withTableName(TABLE_NAME_PERSON)) + .withFieldList(List.of( + new QFieldMetaData("greetingPrefix", QFieldType.STRING), + new QFieldMetaData("greetingSuffix", QFieldType.STRING) + ))) + .withOutputMetaData(new QFunctionOutputMetaData() + .withRecordListMetaData(new QRecordListMetaData() + .withTableName(TABLE_NAME_PERSON) + .withField(new QFieldMetaData("fullGreeting", QFieldType.STRING)) + ) + .withFieldList(List.of(new QFieldMetaData("outputMessage", QFieldType.STRING)))) + ); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -886,4 +962,52 @@ public class TestUtils .withProcessName(PROCESS_NAME_INCREASE_BIRTHDATE)); } + + + /******************************************************************************* + ** + *******************************************************************************/ + public static QReportMetaData defineShapesPersonsReport() + { + return new QReportMetaData() + .withName(REPORT_NAME_SHAPES_PERSON) + .withProcessName(PROCESS_NAME_RUN_SHAPES_PERSON_REPORT) + .withInputFields(List.of( + new QFieldMetaData(RunReportForRecordProcess.FIELD_RECORD_ID, QFieldType.INTEGER).withIsRequired(true) + )) + .withDataSources(List.of( + new QReportDataSource() + .withName("persons") + .withSourceTable(TestUtils.TABLE_NAME_PERSON_MEMORY) + .withQueryFilter(new QQueryFilter() + .withCriteria(new QFilterCriteria("favoriteShapeId", QCriteriaOperator.EQUALS, List.of("${input." + RunReportForRecordProcess.FIELD_RECORD_ID + "}"))) + ) + )) + .withViews(List.of( + new QReportView() + .withName("person") + .withDataSourceName("persons") + .withType(ReportType.TABLE) + .withColumns(List.of( + new QReportField().withName("id"), + new QReportField().withName("firstName"), + new QReportField().withName("lastName") + )) + )); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static QProcessMetaData defineShapesPersonReportProcess() + { + return RunReportForRecordProcess.processMetaDataBuilder() + .withProcessName(PROCESS_NAME_RUN_SHAPES_PERSON_REPORT) + .withReportName(REPORT_NAME_SHAPES_PERSON) + .withTableName(TestUtils.TABLE_NAME_SHAPE) + .getProcessMetaData(); + } + } diff --git a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/APIBackendModule.java b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/APIBackendModule.java index f922fcc9..4a79a30d 100644 --- a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/APIBackendModule.java +++ b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/APIBackendModule.java @@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.module.api; import com.kingsrook.qqq.backend.core.actions.interfaces.CountInterface; import com.kingsrook.qqq.backend.core.actions.interfaces.DeleteInterface; +import com.kingsrook.qqq.backend.core.actions.interfaces.GetInterface; import com.kingsrook.qqq.backend.core.actions.interfaces.InsertInterface; import com.kingsrook.qqq.backend.core.actions.interfaces.QueryInterface; import com.kingsrook.qqq.backend.core.actions.interfaces.UpdateInterface; @@ -31,8 +32,10 @@ import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableBackendDetails; import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface; import com.kingsrook.qqq.backend.module.api.actions.APICountAction; +import com.kingsrook.qqq.backend.module.api.actions.APIGetAction; import com.kingsrook.qqq.backend.module.api.actions.APIInsertAction; import com.kingsrook.qqq.backend.module.api.actions.APIQueryAction; +import com.kingsrook.qqq.backend.module.api.actions.APIUpdateAction; /******************************************************************************* @@ -94,6 +97,17 @@ public class APIBackendModule implements QBackendModuleInterface + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public GetInterface getGetInterface() + { + return (new APIGetAction()); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -111,7 +125,7 @@ public class APIBackendModule implements QBackendModuleInterface @Override public UpdateInterface getUpdateInterface() { - return (null); //return (new RDBMSUpdateAction()); + return (new APIUpdateAction()); } diff --git a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/APICountAction.java b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/APICountAction.java index 81240b42..1433ee8b 100644 --- a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/APICountAction.java +++ b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/APICountAction.java @@ -22,13 +22,11 @@ package com.kingsrook.qqq.backend.module.api.actions; -import java.util.List; import com.kingsrook.qqq.backend.core.actions.interfaces.CountInterface; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput; import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput; 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.tables.QTableMetaData; import org.apache.http.HttpResponse; import org.apache.http.client.HttpClient; @@ -58,23 +56,24 @@ public class APICountAction extends AbstractAPIAction implements CountInterface try { QQueryFilter filter = countInput.getFilter(); - String paramString = apiActionUtil.buildQueryString(filter, null, null, table.getFields()); + String paramString = apiActionUtil.buildQueryStringForGet(filter, null, null, table.getFields()); HttpClientBuilder httpClientBuilder = HttpClientBuilder.create(); HttpClient client = httpClientBuilder.build(); - String url = apiActionUtil.buildTableUrl(table); - HttpGet request = new HttpGet(url + paramString); + String url = apiActionUtil.buildTableUrl(table) + paramString; + LOG.info("API URL: " + url); + HttpGet request = new HttpGet(url); apiActionUtil.setupAuthorizationInRequest(request); apiActionUtil.setupContentTypeInRequest(request); apiActionUtil.setupAdditionalHeaders(request); - HttpResponse response = client.execute(request); - List queryResults = apiActionUtil.processGetResponse(table, response); + HttpResponse response = client.execute(request); + Integer count = apiActionUtil.processGetResponseForCount(table, response); CountOutput rs = new CountOutput(); - rs.setCount(queryResults.size()); + rs.setCount(count); return rs; } catch(Exception e) diff --git a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/APIGetAction.java b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/APIGetAction.java new file mode 100644 index 00000000..a00c1b9d --- /dev/null +++ b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/APIGetAction.java @@ -0,0 +1,83 @@ +/* + * 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.module.api.actions; + + +import com.kingsrook.qqq.backend.core.actions.interfaces.GetInterface; +import com.kingsrook.qqq.backend.core.exceptions.QException; +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.data.QRecord; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import org.apache.http.HttpResponse; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class APIGetAction extends AbstractAPIAction implements GetInterface +{ + private static final Logger LOG = LogManager.getLogger(APIGetAction.class); + + + + /******************************************************************************* + ** + *******************************************************************************/ + public GetOutput execute(GetInput getInput) throws QException + { + QTableMetaData table = getInput.getTable(); + preAction(getInput); + + try + { + String urlSuffix = apiActionUtil.buildUrlSuffixForSingleRecordGet(getInput.getPrimaryKey()); + + HttpClientBuilder httpClientBuilder = HttpClientBuilder.create(); + HttpClient client = httpClientBuilder.build(); + + String url = apiActionUtil.buildTableUrl(table); + HttpGet request = new HttpGet(url + urlSuffix); + + apiActionUtil.setupAuthorizationInRequest(request); + apiActionUtil.setupContentTypeInRequest(request); + apiActionUtil.setupAdditionalHeaders(request); + + HttpResponse response = client.execute(request); + QRecord record = apiActionUtil.processSingleRecordGetResponse(table, response); + + GetOutput rs = new GetOutput(); + rs.setRecord(record); + return rs; + } + catch(Exception e) + { + LOG.warn("Error in API get", e); + throw new QException("Error executing get: " + e.getMessage(), e); + } + } +} diff --git a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/APIQueryAction.java b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/APIQueryAction.java index d7a9aaa2..2f5cc8d8 100644 --- a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/APIQueryAction.java +++ b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/APIQueryAction.java @@ -22,13 +22,11 @@ package com.kingsrook.qqq.backend.module.api.actions; -import java.util.List; import com.kingsrook.qqq.backend.core.actions.interfaces.QueryInterface; import com.kingsrook.qqq.backend.core.exceptions.QException; 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.data.QRecord; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import org.apache.http.HttpResponse; import org.apache.http.client.HttpClient; @@ -55,34 +53,83 @@ public class APIQueryAction extends AbstractAPIAction implements QueryInterface QTableMetaData table = queryInput.getTable(); preAction(queryInput); - try + QueryOutput queryOutput = new QueryOutput(queryInput); + Integer originalLimit = queryInput.getLimit(); + Integer limit = originalLimit; + Integer skip = queryInput.getSkip(); + + if(limit == null) { - QQueryFilter filter = queryInput.getFilter(); - String paramString = apiActionUtil.buildQueryString(filter, queryInput.getLimit(), queryInput.getSkip(), table.getFields()); - - HttpClientBuilder httpClientBuilder = HttpClientBuilder.create(); - HttpClient client = httpClientBuilder.build(); - - String url = apiActionUtil.buildTableUrl(table) + paramString; - HttpGet request = new HttpGet(url); - - LOG.info("API URL: " + url); - - apiActionUtil.setupAuthorizationInRequest(request); - apiActionUtil.setupContentTypeInRequest(request); - apiActionUtil.setupAdditionalHeaders(request); - - HttpResponse response = client.execute(request); - List queryResults = apiActionUtil.processGetResponse(table, response); - - QueryOutput queryOutput = new QueryOutput(queryInput); - queryOutput.addRecords(queryResults); - return (queryOutput); + limit = apiActionUtil.getApiStandardLimit(); } - catch(Exception e) + + int totalCount = 0; + while(true) { - LOG.warn("Error in API Query", e); - throw new QException("Error executing query: " + e.getMessage(), e); + try + { + QQueryFilter filter = queryInput.getFilter(); + String paramString = apiActionUtil.buildQueryStringForGet(filter, limit, skip, table.getFields()); + + HttpClientBuilder httpClientBuilder = HttpClientBuilder.create(); + HttpClient client = httpClientBuilder.build(); + + String url = apiActionUtil.buildTableUrl(table) + paramString; + LOG.info("API URL: " + url); + + /////////////////////////// + // todo - 429 handling!! // + /////////////////////////// + HttpGet request = new HttpGet(url); + + apiActionUtil.setupAuthorizationInRequest(request); + apiActionUtil.setupContentTypeInRequest(request); + apiActionUtil.setupAdditionalHeaders(request); + + HttpResponse response = client.execute(request); + + int count = apiActionUtil.processGetResponse(table, response, queryOutput); + totalCount += count; + + ///////////////////////////////////////////////////////////////////////// + // if we've fetched at least as many as the original limit, then break // + ///////////////////////////////////////////////////////////////////////// + if(originalLimit != null && totalCount >= originalLimit) + { + return (queryOutput); + } + + //////////////////////////////////////////////////////////////////////////////////// + // if we got back less than a full page this time, then we must be done, so break // + //////////////////////////////////////////////////////////////////////////////////// + if(count == 0 || (limit != null && count < limit)) + { + return (queryOutput); + } + + /////////////////////////////////////////////////////////////////// + // if there's an async callback that says we're cancelled, break // + /////////////////////////////////////////////////////////////////// + if(queryInput.getAsyncJobCallback().wasCancelRequested()) + { + LOG.info("Breaking query job, as requested."); + return (queryOutput); + } + + //////////////////////////////////////////////////////////////////////////// + // else, increment the skip by the count we just got, and query for more. // + //////////////////////////////////////////////////////////////////////////// + if(skip == null) + { + skip = 0; + } + skip += count; + } + catch(Exception e) + { + LOG.warn("Error in API Query", e); + throw new QException("Error executing query: " + e.getMessage(), e); + } } } diff --git a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/APIUpdateAction.java b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/APIUpdateAction.java new file mode 100644 index 00000000..7d1f2a86 --- /dev/null +++ b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/APIUpdateAction.java @@ -0,0 +1,194 @@ +/* + * 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.module.api.actions; + + +import java.util.ArrayList; +import java.util.concurrent.TimeUnit; +import com.kingsrook.qqq.backend.core.actions.interfaces.UpdateInterface; +import com.kingsrook.qqq.backend.core.exceptions.QException; +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.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import com.kingsrook.qqq.backend.core.utils.SleepUtils; +import com.kingsrook.qqq.backend.module.api.exceptions.RateLimitException; +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpPut; +import org.apache.http.conn.HttpClientConnectionManager; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import org.apache.http.util.EntityUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class APIUpdateAction extends AbstractAPIAction implements UpdateInterface +{ + private static final Logger LOG = LogManager.getLogger(APIUpdateAction.class); + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public UpdateOutput execute(UpdateInput updateInput) throws QException + { + UpdateOutput updateOutput = new UpdateOutput(); + updateOutput.setRecords(new ArrayList<>()); + + if(CollectionUtils.nullSafeIsEmpty(updateInput.getRecords())) + { + LOG.info("Update request called with 0 records. Returning with no-op"); + return (updateOutput); + } + + QTableMetaData table = updateInput.getTable(); + preAction(updateInput); + + HttpClientConnectionManager connectionManager = null; + try + { + connectionManager = new PoolingHttpClientConnectionManager(); + + // todo - supports bulk put? + + for(QRecord record : updateInput.getRecords()) + { + putRecords(updateOutput, table, connectionManager, record); + + if(updateInput.getRecords().size() > 1 && apiActionUtil.getMillisToSleepAfterEveryCall() > 0) + { + SleepUtils.sleep(apiActionUtil.getMillisToSleepAfterEveryCall(), TimeUnit.MILLISECONDS); + } + } + + return (updateOutput); + } + catch(Exception e) + { + LOG.warn("Error in API Insert for [" + table.getName() + "]", e); + throw new QException("Error executing update: " + e.getMessage(), e); + } + finally + { + if(connectionManager != null) + { + connectionManager.shutdown(); + } + } + + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private void putRecords(UpdateOutput updateOutput, QTableMetaData table, HttpClientConnectionManager connectionManager, QRecord record) throws RateLimitException + { + int sleepMillis = apiActionUtil.getInitialRateLimitBackoffMillis(); + int rateLimitsCaught = 0; + while(true) + { + try + { + putOneTime(updateOutput, table, connectionManager, record); + return; + } + catch(RateLimitException rle) + { + rateLimitsCaught++; + if(rateLimitsCaught > apiActionUtil.getMaxAllowedRateLimitErrors()) + { + LOG.warn("Giving up PUT to [" + table.getName() + "] after too many rate-limit errors (" + apiActionUtil.getMaxAllowedRateLimitErrors() + ")"); + record.addError("Error: " + rle.getMessage()); + updateOutput.addRecord(record); + return; + } + + LOG.info("Caught RateLimitException [#" + rateLimitsCaught + "] PUT'ing to [" + table.getName() + "] - sleeping [" + sleepMillis + "]..."); + SleepUtils.sleep(sleepMillis, TimeUnit.MILLISECONDS); + sleepMillis *= 2; + } + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private void putOneTime(UpdateOutput insertOutput, QTableMetaData table, HttpClientConnectionManager connectionManager, QRecord record) throws RateLimitException + { + try + { + CloseableHttpClient client = HttpClients.custom().setConnectionManager(connectionManager).build(); + + String url = buildTableUrl(table); + url += record.getValueString("number"); + HttpPut request = new HttpPut(url); + apiActionUtil.setupAuthorizationInRequest(request); + apiActionUtil.setupContentTypeInRequest(request); + apiActionUtil.setupAdditionalHeaders(request); + + request.setEntity(apiActionUtil.recordToEntity(table, record)); + + HttpResponse response = client.execute(request); + int statusCode = response.getStatusLine().getStatusCode(); + if(statusCode == 429) + { + throw (new RateLimitException(EntityUtils.toString(response.getEntity()))); + } + + QRecord outputRecord = apiActionUtil.processPostResponse(table, record, response); + insertOutput.addRecord(outputRecord); + } + catch(RateLimitException rle) + { + throw (rle); + } + catch(Exception e) + { + LOG.warn("Error posting to [" + table.getName() + "]", e); + record.addError("Error: " + e.getMessage()); + insertOutput.addRecord(record); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + protected String buildTableUrl(QTableMetaData table) + { + return (backendMetaData.getBaseUrl() + "/orders/SalesOrder/"); + } + +} diff --git a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/BaseAPIActionUtil.java b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/BaseAPIActionUtil.java index ce277999..e78fe58f 100644 --- a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/BaseAPIActionUtil.java +++ b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/BaseAPIActionUtil.java @@ -26,20 +26,23 @@ import java.io.IOException; import java.io.Serializable; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; -import java.util.ArrayList; import java.util.Base64; import java.util.List; import java.util.Map; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput; +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.actions.tables.query.QueryInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput; import com.kingsrook.qqq.backend.core.model.data.QRecord; -import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; 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.StringUtils; +import com.kingsrook.qqq.backend.core.utils.ValueUtils; import com.kingsrook.qqq.backend.module.api.model.metadata.APIBackendMetaData; import com.kingsrook.qqq.backend.module.api.model.metadata.APITableBackendDetails; import org.apache.http.HttpEntity; @@ -72,7 +75,7 @@ public class BaseAPIActionUtil *******************************************************************************/ public long getMillisToSleepAfterEveryCall() { - return 0; + return (0); } @@ -82,7 +85,7 @@ public class BaseAPIActionUtil *******************************************************************************/ public int getInitialRateLimitBackoffMillis() { - return 0; + return (0); } @@ -92,7 +95,17 @@ public class BaseAPIActionUtil *******************************************************************************/ public int getMaxAllowedRateLimitErrors() { - return 0; + return (0); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public Integer getApiStandardLimit() + { + return (20); } @@ -101,7 +114,7 @@ public class BaseAPIActionUtil ** method to build up a query string based on a given QFilter object ** *******************************************************************************/ - protected String buildQueryString(QQueryFilter filter, Integer limit, Integer skip, Map fields) throws QException + protected String buildQueryStringForGet(QQueryFilter filter, Integer limit, Integer skip, Map fields) throws QException { // todo: reasonable default action return (null); @@ -109,6 +122,19 @@ public class BaseAPIActionUtil + /******************************************************************************* + ** Do a default query string for a single-record GET - e.g., a query for just 1 record. + *******************************************************************************/ + public String buildUrlSuffixForSingleRecordGet(Serializable primaryKey) throws QException + { + QTableMetaData table = actionInput.getTable(); + QQueryFilter filter = new QQueryFilter() + .withCriteria(new QFilterCriteria(table.getPrimaryKeyField(), QCriteriaOperator.EQUALS, List.of(primaryKey))); + return (buildQueryStringForGet(filter, 1, 0, table.getFields())); + } + + + /******************************************************************************* ** As part of making a request - set up its authorization header (not just ** strictly "Authorization", but whatever is needed for auth). @@ -205,6 +231,14 @@ public class BaseAPIActionUtil { JSONObject body = recordToJsonObject(table, record); String json = body.toString(); + + String tablePath = getBackendDetails(table).getTablePath(); + if(tablePath != null) + { + body = new JSONObject(); + body.put(tablePath, new JSONObject(json)); + json = body.toString(); + } LOG.debug(json); return (new StringEntity(json)); } @@ -248,7 +282,8 @@ public class BaseAPIActionUtil *******************************************************************************/ protected QRecord jsonObjectToRecord(JSONObject jsonObject, Map fields) throws IOException { - QRecord record = JsonUtils.parseQRecord(jsonObject, fields); + QRecord record = JsonUtils.parseQRecord(jsonObject, fields, true); + record.getBackendDetails().put(QRecord.BACKEND_DETAILS_TYPE_JSON_SOURCE_OBJECT, jsonObject.toString()); return (record); } @@ -257,16 +292,21 @@ public class BaseAPIActionUtil /******************************************************************************* ** *******************************************************************************/ - protected List processGetResponse(QTableMetaData table, HttpResponse response) throws IOException + protected int processGetResponse(QTableMetaData table, HttpResponse response, QueryOutput queryOutput) throws IOException { int statusCode = response.getStatusLine().getStatusCode(); System.out.println(statusCode); + if(statusCode >= 400) + { + handleGetResponseError(table, response); + } + HttpEntity entity = response.getEntity(); String resultString = EntityUtils.toString(entity); - List recordList = new ArrayList<>(); - if(StringUtils.hasContent(resultString)) + int count = 0; + if(StringUtils.hasContent(resultString) && !resultString.equals("null")) { JSONArray resultList = null; JSONObject jsonObject = null; @@ -289,16 +329,30 @@ public class BaseAPIActionUtil { for(int i = 0; i < resultList.length(); i++) { - recordList.add(jsonObjectToRecord(resultList.getJSONObject(i), table.getFields())); + queryOutput.addRecord(jsonObjectToRecord(resultList.getJSONObject(i), table.getFields())); + count++; } } else { - recordList.add(jsonObjectToRecord(jsonObject, table.getFields())); + queryOutput.addRecord(jsonObjectToRecord(jsonObject, table.getFields())); + count++; } } - return (recordList); + return (count); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private void handleGetResponseError(QTableMetaData table, HttpResponse response) throws IOException + { + HttpEntity entity = response.getEntity(); + String resultString = EntityUtils.toString(entity); + throw new IOException("Error performing query: " + resultString); } @@ -308,14 +362,7 @@ public class BaseAPIActionUtil *******************************************************************************/ protected QRecord processPostResponse(QTableMetaData table, QRecord record, HttpResponse response) throws IOException { - int statusCode = response.getStatusLine().getStatusCode(); - LOG.debug(statusCode); - - HttpEntity entity = response.getEntity(); - String resultString = EntityUtils.toString(entity); - LOG.debug(resultString); - - JSONObject jsonObject = JsonUtils.toJSONObject(resultString); + JSONObject jsonObject = getJsonObject(response); String primaryKeyFieldName = table.getPrimaryKeyField(); String primaryKeyBackendName = getFieldBackendName(table.getField(primaryKeyFieldName)); @@ -346,6 +393,24 @@ public class BaseAPIActionUtil + /******************************************************************************* + ** + *******************************************************************************/ + protected JSONObject getJsonObject(HttpResponse response) throws IOException + { + int statusCode = response.getStatusLine().getStatusCode(); + LOG.debug(statusCode); + + HttpEntity entity = response.getEntity(); + String resultString = EntityUtils.toString(entity); + LOG.debug(resultString); + + JSONObject jsonObject = JsonUtils.toJSONObject(resultString); + return jsonObject; + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -418,8 +483,36 @@ public class BaseAPIActionUtil /******************************************************************************* ** *******************************************************************************/ - protected String urlEncode(String s) + protected String urlEncode(Serializable s) { - return (URLEncoder.encode(s, StandardCharsets.UTF_8)); + return (URLEncoder.encode(ValueUtils.getValueAsString(s), StandardCharsets.UTF_8)); } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public QRecord processSingleRecordGetResponse(QTableMetaData table, HttpResponse response) throws IOException + { + return (jsonObjectToRecord(getJsonObject(response), table.getFields())); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public Integer processGetResponseForCount(QTableMetaData table, HttpResponse response) throws IOException + { + ///////////////////////////////////////////////////////////////////////////////////////// + // set up a query output with a blank query input - e.g., one that isn't using a pipe. // + ///////////////////////////////////////////////////////////////////////////////////////// + QueryOutput queryOutput = new QueryOutput(new QueryInput()); + processGetResponse(table, response, queryOutput); + List records = queryOutput.getRecords(); + + return (records == null ? null : records.size()); + } + } diff --git a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/model/metadata/APIBackendMetaData.java b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/model/metadata/APIBackendMetaData.java index c90b7b20..773b61f8 100644 --- a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/model/metadata/APIBackendMetaData.java +++ b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/model/metadata/APIBackendMetaData.java @@ -24,8 +24,10 @@ package com.kingsrook.qqq.backend.module.api.model.metadata; import java.io.Serializable; import java.util.HashMap; +import com.kingsrook.qqq.backend.core.instances.QInstanceValidator; import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; +import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.module.api.APIBackendModule; import com.kingsrook.qqq.backend.module.api.model.AuthorizationType; @@ -379,4 +381,14 @@ public class APIBackendMetaData extends QBackendMetaData return (this); } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void performValidation(QInstanceValidator qInstanceValidator) + { + qInstanceValidator.assertCondition(StringUtils.hasContent(baseUrl), "Missing baseUrl for API backend: " + getName()); + } } diff --git a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/utils/QueryStringBuilder.java b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/utils/QueryStringBuilder.java new file mode 100644 index 00000000..48167586 --- /dev/null +++ b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/utils/QueryStringBuilder.java @@ -0,0 +1,133 @@ +/* + * 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.module.api.utils; + + +import java.io.Serializable; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import com.kingsrook.qqq.backend.core.utils.Pair; +import com.kingsrook.qqq.backend.core.utils.ValueUtils; + + +/******************************************************************************* + ** Utility for building a query string - taking care of things like: + ** - do I need the "?" + ** - do I need a "&" + ** - urlEncoding params (depending on which method you call: (name, value) does + ** encode -- (pair) does not.) + *******************************************************************************/ +public class QueryStringBuilder +{ + private List> pairs = new ArrayList<>(); + + + + /******************************************************************************* + ** Assumes both name and value have NOT been previous URL Encoded + *******************************************************************************/ + public void addPair(String name, Serializable value) + { + String valueString = urlEncode(ValueUtils.getValueAsString(value)); + pairs.add(new Pair<>(urlEncode(name), valueString)); + } + + + + /******************************************************************************* + ** Assumes both name and value have NOT been previous URL Encoded + *******************************************************************************/ + public QueryStringBuilder withPair(String name, Serializable value) + { + addPair(name, value); + return (this); + } + + + + /******************************************************************************* + ** Assumes both parts are already properly uri encoded + *******************************************************************************/ + public void addPair(String pair) + { + String[] parts = pair.split("=", 2); + if(parts.length == 1) + { + pairs.add(new Pair<>(parts[0], "")); + } + else + { + pairs.add(new Pair<>(parts[0], parts[1])); + } + } + + + + /******************************************************************************* + ** Assumes both parts are already properly uri encoded + *******************************************************************************/ + public QueryStringBuilder withPair(String pair) + { + addPair(pair); + return (this); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public String toQueryString() + { + if(pairs.isEmpty()) + { + return (""); + } + + return ("?" + pairs.stream().map(p -> p.getA() + "=" + p.getB()).collect(Collectors.joining("&"))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public String toString() + { + return (toQueryString()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private String urlEncode(Serializable s) + { + return (URLEncoder.encode(ValueUtils.getValueAsString(s), StandardCharsets.UTF_8)); + } + +} diff --git a/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/EasyPostApiTest.java b/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/EasyPostApiTest.java index 81e4427a..16d98fc9 100644 --- a/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/EasyPostApiTest.java +++ b/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/EasyPostApiTest.java @@ -32,9 +32,8 @@ 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.session.QSession; import com.kingsrook.qqq.backend.module.api.model.metadata.APIBackendMetaData; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.DisabledOnOs; -import org.junit.jupiter.api.condition.OS; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; @@ -43,7 +42,7 @@ import static org.junit.jupiter.api.Assertions.assertNull; /******************************************************************************* ** *******************************************************************************/ -@DisabledOnOs(OS.LINUX) +@Disabled // OnOs(OS.LINUX) public class EasyPostApiTest { diff --git a/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/model/metadata/APIBackendMetaDataTest.java b/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/model/metadata/APIBackendMetaDataTest.java new file mode 100644 index 00000000..4ac4d52d --- /dev/null +++ b/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/model/metadata/APIBackendMetaDataTest.java @@ -0,0 +1,51 @@ +/* + * 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.module.api.model.metadata; + + +import com.kingsrook.qqq.backend.core.instances.QInstanceValidator; +import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; + + +/******************************************************************************* + ** Unit test for APIBackendMetaData + *******************************************************************************/ +class APIBackendMetaDataTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void test() + { + APIBackendMetaData apiBackendMetaData = new APIBackendMetaData() + .withName("test"); + QInstanceValidator qInstanceValidator = new QInstanceValidator(); + apiBackendMetaData.performValidation(qInstanceValidator); + assertEquals(1, qInstanceValidator.getErrors().size()); + assertThat(qInstanceValidator.getErrors()).anyMatch(e -> e.contains("Missing baseUrl")); + } + +} \ No newline at end of file diff --git a/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/utils/QueryStringBuilderTest.java b/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/utils/QueryStringBuilderTest.java new file mode 100644 index 00000000..6427f15c --- /dev/null +++ b/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/utils/QueryStringBuilderTest.java @@ -0,0 +1,113 @@ +/* + * 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.module.api.utils; + + +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; + + +/******************************************************************************* + ** Unit test for QueryStringBuilder + *******************************************************************************/ +class QueryStringBuilderTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testEmpty() + { + assertEquals("", new QueryStringBuilder().toQueryString()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testSimpleWithoutAnd() + { + QueryStringBuilder queryStringBuilder = new QueryStringBuilder(); + queryStringBuilder.addPair("foo", 1); + assertEquals("?foo=1", queryStringBuilder.toQueryString()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testSimpleWithAnd() + { + QueryStringBuilder queryStringBuilder = new QueryStringBuilder(); + queryStringBuilder.addPair("foo", 1); + queryStringBuilder.addPair("bar=2"); + assertEquals("?foo=1&bar=2", queryStringBuilder.toQueryString()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testFluent() + { + assertEquals("?foo=1&bar=2", new QueryStringBuilder() + .withPair("foo", 1) + .withPair("bar=2") + .toQueryString()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testEncoding() + { + QueryStringBuilder queryStringBuilder = new QueryStringBuilder(); + queryStringBuilder.addPair("percent", "99%"); // % should get encoded to %25 + queryStringBuilder.addPair("and=this%26that"); // %26 should stay as-is -- not be re-encoded. + assertEquals("?percent=99%25&and=this%26that", queryStringBuilder.toQueryString()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testPairWithoutValue() + { + QueryStringBuilder queryStringBuilder = new QueryStringBuilder(); + queryStringBuilder.addPair("name1"); + queryStringBuilder.addPair("name2="); + assertEquals("?name1=&name2=", queryStringBuilder.toString()); + } + +} \ No newline at end of file diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/AbstractRDBMSAction.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/AbstractRDBMSAction.java index 4f6d6331..94a09dbe 100644 --- a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/AbstractRDBMSAction.java +++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/AbstractRDBMSAction.java @@ -401,4 +401,14 @@ public abstract class AbstractRDBMSAction implements QActionInterface } } + + + /******************************************************************************* + ** + *******************************************************************************/ + protected String escapeIdentifier(String id) + { + return ("`" + id + "`"); + } + } diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSCountAction.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSCountAction.java index 3a568f95..92b4d752 100644 --- a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSCountAction.java +++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSCountAction.java @@ -57,7 +57,7 @@ public class RDBMSCountAction extends AbstractRDBMSAction implements CountInterf QTableMetaData table = countInput.getTable(); String tableName = getTableName(table); - String sql = "SELECT count(*) as record_count FROM " + tableName; + String sql = "SELECT count(*) as record_count FROM " + escapeIdentifier(tableName); QQueryFilter filter = countInput.getFilter(); List params = new ArrayList<>(); diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSDeleteAction.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSDeleteAction.java index 6753fe19..36efffa4 100644 --- a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSDeleteAction.java +++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSDeleteAction.java @@ -186,7 +186,7 @@ public class RDBMSDeleteAction extends AbstractRDBMSAction implements DeleteInte // todo sql customization - can edit sql and/or param list? String sql = "DELETE FROM " - + tableName + + escapeIdentifier(tableName) + " WHERE " + primaryKeyName + " = ?"; diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSInsertAction.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSInsertAction.java index f4a08148..9e3b8450 100644 --- a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSInsertAction.java +++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSInsertAction.java @@ -109,7 +109,7 @@ public class RDBMSInsertAction extends AbstractRDBMSAction implements InsertInte { for(List page : CollectionUtils.getPages(insertInput.getRecords(), QueryManager.PAGE_SIZE)) { - String tableName = getTableName(table); + String tableName = escapeIdentifier(getTableName(table)); StringBuilder sql = new StringBuilder("INSERT INTO ").append(tableName).append("(").append(columns).append(") VALUES"); List params = new ArrayList<>(); int recordIndex = 0; diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryAction.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryAction.java index ad5e42d2..c326f865 100644 --- a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryAction.java +++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryAction.java @@ -72,7 +72,7 @@ public class RDBMSQueryAction extends AbstractRDBMSAction implements QueryInterf .map(this::getColumnName) .collect(Collectors.joining(", ")); - String sql = "SELECT " + columns + " FROM " + tableName; + String sql = "SELECT " + columns + " FROM " + escapeIdentifier(tableName); QQueryFilter filter = queryInput.getFilter(); List params = new ArrayList<>(); 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 05026f16..0e03ba60 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 @@ -223,7 +223,7 @@ public class RDBMSUpdateAction extends AbstractRDBMSAction implements UpdateInte .map(f -> this.getColumnName(table.getField(f)) + " = ?") .collect(Collectors.joining(", ")); - String tableName = getTableName(table); + String tableName = escapeIdentifier(getTableName(table)); return ("UPDATE " + tableName + " SET " + columns + " WHERE " + getColumnName(table.getField(table.getPrimaryKeyField())) + " "); diff --git a/qqq-dev-tools/MODULE_LIST b/qqq-dev-tools/MODULE_LIST index 8c54c8af..1d7132d4 100644 --- a/qqq-dev-tools/MODULE_LIST +++ b/qqq-dev-tools/MODULE_LIST @@ -2,6 +2,7 @@ qqq-backend-core qqq-backend-module-api qqq-backend-module-rdbms qqq-backend-module-filesystem +qqq-language-support-javascript qqq-middleware-javalin qqq-middleware-picocli qqq-middleware-lambda diff --git a/qqq-dev-tools/bin/reset-snapshot-deps.sh b/qqq-dev-tools/bin/reset-snapshot-deps.sh index 77240dee..ddfe2aa1 100755 --- a/qqq-dev-tools/bin/reset-snapshot-deps.sh +++ b/qqq-dev-tools/bin/reset-snapshot-deps.sh @@ -9,5 +9,9 @@ CURRENT_VERSION="$(cat $QQQ_DEV_TOOLS_DIR/CURRENT-SNAPSHOT-VERSION)" MODULE_LIST_FILE=$QQQ_DEV_TOOLS_DIR/MODULE_LIST for artifact in $(cat $MODULE_LIST_FILE); do - update-dep.sh $artifact ${CURRENT_VERSION}-SNAPSHOT + update-dep.sh $artifact ${CURRENT_VERSION}-SNAPSHOT -q done + +echo +echo git diff pom.xml +git diff pom.xml diff --git a/qqq-dev-tools/bin/update-all-qqq-deps.sh b/qqq-dev-tools/bin/update-all-qqq-deps.sh index 09cad332..4e19bcd4 100755 --- a/qqq-dev-tools/bin/update-all-qqq-deps.sh +++ b/qqq-dev-tools/bin/update-all-qqq-deps.sh @@ -9,6 +9,14 @@ CURRENT_VERSION="$(cat $QQQ_DEV_TOOLS_DIR/CURRENT-SNAPSHOT-VERSION)" MODULE_LIST_FILE=$QQQ_DEV_TOOLS_DIR/MODULE_LIST for module in $(cat $MODULE_LIST_FILE); do + echo "Updating $module..." version=$(get-latest-snapshot.sh $module $CURRENT_VERSION) - update-dep.sh $module $version + update-dep.sh $module $version -q done + +echo +echo git diff pom.xml +git diff pom.xml +newVersion=$(grep -A1 qqq-backend-core pom.xml | tail -1 | sed 's/.*//;s/<\/version>.*//;s/-........\......./ snapshot/') +echo "You might want to commit that with:" +echo " git commit -m \"Updated qqq deps to $newVersion\" pom.xml" diff --git a/qqq-dev-tools/bin/update-dep.sh b/qqq-dev-tools/bin/update-dep.sh index 22b6ae7d..1fdf54d2 100755 --- a/qqq-dev-tools/bin/update-dep.sh +++ b/qqq-dev-tools/bin/update-dep.sh @@ -8,6 +8,11 @@ dep=$1 version=$2 +verbose=1 +if [ "$3" == "-q" ]; then + verbose=0; +fi + if [ -z "$dep" -o -z "$version" ]; then echo "What dependency?" @@ -38,7 +43,12 @@ if [ $lineNo -lt $dependenciesTagLineNo ]; then exit 0; fi -echo "Going to update version of $dep at line $lineNo" +if [ "$verbose" == "1" ]; then + echo "Going to update version of $dep at line $lineNo" +fi + gsed -i "${lineNo}s/.*$version + + + + 4.0.0 + + qqq-language-support-javascript + + + com.kingsrook.qqq + qqq-parent-project + ${revision} + + + + + + + 0.10 + 0.10 + + + + + + com.kingsrook.qqq + qqq-backend-core + ${revision} + + + + + org.openjdk.nashorn + nashorn-core + 15.4 + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + + + org.apache.logging.log4j + log4j-api + + + org.apache.logging.log4j + log4j-core + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.assertj + assertj-core + test + + + + + + + + + + + + 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 new file mode 100644 index 00000000..3c28a026 --- /dev/null +++ b/qqq-language-support-javascript/src/main/java/com/kingsrook/qqq/languages/javascript/QJavaScriptExecutor.java @@ -0,0 +1,172 @@ +/* + * 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.languages.javascript; + + +import javax.script.Bindings; +import javax.script.ScriptEngine; +import javax.script.ScriptEngineManager; +import javax.script.ScriptException; +import java.io.Serializable; +import java.util.Map; +import com.kingsrook.qqq.backend.core.actions.scripts.QCodeExecutor; +import com.kingsrook.qqq.backend.core.actions.scripts.logging.QCodeExecutionLoggerInterface; +import com.kingsrook.qqq.backend.core.exceptions.QCodeException; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; +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.internal.runtime.ECMAException; +import org.openjdk.nashorn.internal.runtime.ParserException; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class QJavaScriptExecutor implements QCodeExecutor +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public Serializable execute(QCodeReference codeReference, Map inputContext, QCodeExecutionLoggerInterface executionLogger) throws QCodeException + { + String code = getCode(codeReference); + Serializable output = runInline(code, inputContext, executionLogger); + return (output); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private Serializable runInline(String code, Map inputContext, QCodeExecutionLoggerInterface executionLogger) throws QCodeException + { + new NashornScriptEngineFactory(); + ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn"); + + ////////////////////////////////////////////// + // setup the javascript environment/context // + ////////////////////////////////////////////// + Bindings bindings = engine.createBindings(); + bindings.putAll(inputContext); + + if(!bindings.containsKey("logger")) + { + bindings.put("logger", executionLogger); + } + + //////////////////////////////////////////////////////////////////////// + // wrap the user's code in an immediately-invoked function expression // + //////////////////////////////////////////////////////////////////////// + String codeToRun = """ + (function userDefinedFunction() + { + %s + })(); + """.formatted(code); + + Serializable output; + try + { + output = (Serializable) engine.eval(codeToRun, bindings); + } + catch(ScriptException se) + { + QCodeException qCodeException = getQCodeExceptionFromScriptException(se); + throw (qCodeException); + } + + return (output); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private QCodeException getQCodeExceptionFromScriptException(ScriptException se) + { + boolean isParserException = ExceptionUtils.findClassInRootChain(se, ParserException.class) != null; + boolean isUserThrownException = ExceptionUtils.findClassInRootChain(se, ECMAException.class) != null; + + String message = se.getMessage(); + String errorContext = null; + if(message != null) + { + message = message.replaceFirst(" in .*", ""); + message = message.replaceFirst(":\\d+:\\d+", ""); + + if(message.contains("\n")) + { + String[] parts = message.split("\n", 2); + message = parts[0]; + errorContext = parts[1]; + } + } + + int actualScriptLineNumber = se.getLineNumber() - 2; + + String prefix = "Script Exception"; + boolean includeColumn = true; + boolean includeContext = false; + if(isParserException) + { + prefix = "Script parser exception"; + includeContext = true; + } + else if(isUserThrownException) + { + prefix = "Script threw an exception"; + includeColumn = false; + } + + QCodeException qCodeException = new QCodeException(prefix + " at line " + actualScriptLineNumber + (includeColumn ? (" column " + se.getColumnNumber()) : "") + ": " + message); + if(includeContext) + { + qCodeException.setContext(errorContext); + } + + return (qCodeException); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private String getCode(QCodeReference codeReference) + { + if(StringUtils.hasContent(codeReference.getInlineCode())) + { + return (codeReference.getInlineCode()); + } + else + { + throw (new NotImplementedException("Only inline code is implemented at this time.")); + } + } + +} 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 new file mode 100644 index 00000000..192379c7 --- /dev/null +++ b/qqq-language-support-javascript/src/test/java/com/kingsrook/qqq/languages/javascript/ExecuteCodeActionTest.java @@ -0,0 +1,322 @@ +/* + * 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.languages.javascript; + + +import java.io.Serializable; +import com.kingsrook.qqq.backend.core.actions.scripts.ExecuteCodeAction; +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; +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.model.metadata.code.QCodeUsage; +import com.kingsrook.qqq.backend.core.model.session.QSession; +import org.assertj.core.api.Assertions; +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.assertTrue; + + +/******************************************************************************* + ** Unit test for ExecuteCodeAction + *******************************************************************************/ +class ExecuteCodeActionTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testHelloWorld() throws QException + { + ExecuteCodeInput input = new ExecuteCodeInput(TestUtils.defineInstance()) + .withCodeReference(new QCodeReference("helloWorld.js", QCodeType.JAVA_SCRIPT, QCodeUsage.CUSTOMIZER) + .withInlineCode(""" + return "Hello, " + input""")) + .withContext("input", "World"); + ExecuteCodeOutput output = new ExecuteCodeOutput(); + new ExecuteCodeAction().run(input, output); + assertEquals("Hello, World", output.getOutput()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testSetInContextObject() throws QException + { + OneTestOutput oneTestOutput = testOne(3, """ + var a = 1; + var b = 2; + output.setD(a + b + input.getC()); + """); + assertEquals(6, oneTestOutput.testOutput().getD()); + assertNull(oneTestOutput.executeCodeOutput().getOutput()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testReturnsContextObject() throws QException + { + OneTestOutput oneTestOutput = testOne(4, """ + var a = 1; + var b = 2; + output.setD(a + b + input.getC()); + return (output); + """); + assertEquals(7, oneTestOutput.testOutput().getD()); + assertTrue(oneTestOutput.executeCodeOutput().getOutput() instanceof TestOutput); + assertEquals(7, ((TestOutput) oneTestOutput.executeCodeOutput().getOutput()).getD()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testReturnsPrimitive() throws QException + { + OneTestOutput oneTestOutput = testOne(5, """ + var a = 1; + var b = 2; + output.setD(a + b + input.getC()); + return output.getD() + """); + assertEquals(8, oneTestOutput.testOutput().getD()); + assertEquals(8, oneTestOutput.executeCodeOutput().getOutput()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testThrows() throws QException + { + String code = """ + var a = 1; + var b = 2; + if (input.getC() === 6) + { + throw ("oh no, six!"); + } + output.setD(a + b + input.getC()); + return output.getD() + """; + + Assertions.assertThatThrownBy(() -> testOne(6, code)) + .isInstanceOf(QCodeException.class) + .hasMessageContaining("threw") + .hasMessageContaining("oh no, six!") + .hasMessageContaining("line 5:"); + + OneTestOutput oneTestOutput = testOne(7, code); + assertEquals(10, oneTestOutput.testOutput().getD()); + assertEquals(10, oneTestOutput.executeCodeOutput().getOutput()); + + Assertions.assertThatThrownBy(() -> testOne(6, """ + var a = null; + return a.toString(); + """)) + .isInstanceOf(QCodeException.class) + .hasMessageContaining("threw") + .hasMessageContaining("TypeError: null has no such function \"toString\""); + + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testSyntaxError() throws QException + { + Assertions.assertThatThrownBy(() -> testOne(6, """ + var a = 1; + if (input.getC() === 6 + { + """)) + .isInstanceOf(QCodeException.class) + .hasMessageContaining("parser") + .hasMessageContaining("line 3 column 0"); + + Assertions.assertThatThrownBy(() -> testOne(6, """ + var a = 1; + vr b = 2; + """)) + .isInstanceOf(QCodeException.class) + .hasMessageContaining("parser") + .hasMessageContaining("line 2 column 3"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testLogs() throws QException + { + OneTestOutput oneTestOutput = testOne(5, """ + logger.log("This is a log."); + """); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private OneTestOutput testOne(Integer inputValueC, String code) throws QException + { + System.out.println(); + QInstance instance = TestUtils.defineInstance(); + + TestInput testInput = new TestInput(); + testInput.setC(inputValueC); + + TestOutput testOutput = new TestOutput(); + + ExecuteCodeInput input = new ExecuteCodeInput(instance); + input.setSession(new QSession()); + input.setCodeReference(new QCodeReference("test.js", QCodeType.JAVA_SCRIPT, QCodeUsage.CUSTOMIZER).withInlineCode(code)); + input.withContext("input", testInput); + input.withContext("output", testOutput); + + ExecuteCodeOutput output = new ExecuteCodeOutput(); + + ExecuteCodeAction executeCodeAction = new ExecuteCodeAction(); + executeCodeAction.run(input, output); + + return (new OneTestOutput(output, testOutput)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private record OneTestOutput(ExecuteCodeOutput executeCodeOutput, TestOutput testOutput) + { + + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static class TestInput implements Serializable + { + private Integer c; + + + + /******************************************************************************* + ** Getter for c + ** + *******************************************************************************/ + public Integer getC() + { + return c; + } + + + + /******************************************************************************* + ** Setter for c + ** + *******************************************************************************/ + public void setC(Integer c) + { + this.c = c; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public String toString() + { + return "TestInput{c=" + c + '}'; + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static class TestOutput implements Serializable + { + private Integer d; + + + + /******************************************************************************* + ** Getter for d + ** + *******************************************************************************/ + public Integer getD() + { + return d; + } + + + + /******************************************************************************* + ** Setter for d + ** + *******************************************************************************/ + public void setD(Integer d) + { + this.d = d; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public String toString() + { + return "TestOutput{d=" + d + '}'; + } + } + +} \ No newline at end of file diff --git a/qqq-language-support-javascript/src/test/java/com/kingsrook/qqq/languages/javascript/TestUtils.java b/qqq-language-support-javascript/src/test/java/com/kingsrook/qqq/languages/javascript/TestUtils.java new file mode 100644 index 00000000..8d414889 --- /dev/null +++ b/qqq-language-support-javascript/src/test/java/com/kingsrook/qqq/languages/javascript/TestUtils.java @@ -0,0 +1,100 @@ +/* + * 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.languages.javascript; + + +import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType; +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.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.metadata.QAuthenticationMetaData; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class TestUtils +{ + public static final String DEFAULT_BACKEND_NAME = "memoryBackend"; + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static QInstance defineInstance() + { + QInstance qInstance = new QInstance(); + qInstance.addBackend(defineBackend()); + qInstance.addTable(defineTablePerson()); + qInstance.setAuthentication(defineAuthentication()); + return (qInstance); + } + + + + /******************************************************************************* + ** Define the authentication used in standard tests - using 'mock' type. + ** + *******************************************************************************/ + public static QAuthenticationMetaData defineAuthentication() + { + return new QAuthenticationMetaData() + .withName("mock") + .withType(QAuthenticationType.MOCK); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static QBackendMetaData defineBackend() + { + return (new QBackendMetaData() + .withName(DEFAULT_BACKEND_NAME) + .withBackendType("memory")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static QTableMetaData defineTablePerson() + { + return new QTableMetaData() + .withName("person") + .withLabel("Person") + .withBackendName(DEFAULT_BACKEND_NAME) + .withPrimaryKeyField("id") + .withField(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false)) + .withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME).withIsEditable(false)) + .withField(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME).withIsEditable(false)) + .withField(new QFieldMetaData("firstName", QFieldType.STRING)) + .withField(new QFieldMetaData("lastName", QFieldType.STRING)) + .withField(new QFieldMetaData("birthDate", QFieldType.DATE)) + .withField(new QFieldMetaData("email", QFieldType.STRING)); + } +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java index 1f1cbe7a..9af73f36 100644 --- a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java @@ -42,8 +42,11 @@ import com.kingsrook.qqq.backend.core.actions.metadata.MetaDataAction; import com.kingsrook.qqq.backend.core.actions.metadata.ProcessMetaDataAction; import com.kingsrook.qqq.backend.core.actions.metadata.TableMetaDataAction; import com.kingsrook.qqq.backend.core.actions.reporting.ExportAction; +import com.kingsrook.qqq.backend.core.actions.scripts.StoreAssociatedScriptAction; +import com.kingsrook.qqq.backend.core.actions.scripts.TestScriptAction; 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; import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction; @@ -65,14 +68,21 @@ import com.kingsrook.qqq.backend.core.model.actions.metadata.TableMetaDataInput; import com.kingsrook.qqq.backend.core.model.actions.metadata.TableMetaDataOutput; import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportInput; import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormat; +import com.kingsrook.qqq.backend.core.model.actions.scripts.StoreAssociatedScriptInput; +import com.kingsrook.qqq.backend.core.model.actions.scripts.StoreAssociatedScriptOutput; +import com.kingsrook.qqq.backend.core.model.actions.scripts.TestScriptInput; +import com.kingsrook.qqq.backend.core.model.actions.scripts.TestScriptOutput; import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput; import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput; import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput; import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteOutput; +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.insert.InsertInput; import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput; 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; @@ -85,11 +95,13 @@ import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetOutput; import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; +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.session.QSession; import com.kingsrook.qqq.backend.core.modules.authentication.Auth0AuthenticationModule; import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleDispatcher; import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleInterface; +import com.kingsrook.qqq.backend.core.processes.utils.GeneralProcessUtils; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.ExceptionUtils; import com.kingsrook.qqq.backend.core.utils.JsonUtils; @@ -270,6 +282,7 @@ public class QJavalinImplementation { get("", QJavalinImplementation::processMetaData); }); + get("/authentication", QJavalinImplementation::authenticationMetaData); }); ///////////////////////// @@ -293,6 +306,11 @@ public class QJavalinImplementation patch("", QJavalinImplementation::dataUpdate); put("", QJavalinImplementation::dataUpdate); // todo - want different semantics?? delete("", QJavalinImplementation::dataDelete); + + get("/developer", QJavalinImplementation::getRecordDeveloperMode); + post("/developer/associatedScript/{fieldName}", QJavalinImplementation::storeRecordAssociatedScript); + get("/developer/associatedScript/{fieldName}/{scriptRevisionId}/logs", QJavalinImplementation::getAssociatedScriptLogs); + post("/developer/associatedScript/{fieldName}/test", QJavalinImplementation::testAssociatedScript); }); }); @@ -307,6 +325,16 @@ public class QJavalinImplementation + /******************************************************************************* + ** + *******************************************************************************/ + private static void authenticationMetaData(Context context) + { + context.result(JsonUtils.toJson(qInstance.getAuthentication())); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -501,38 +529,31 @@ public class QJavalinImplementation String tableName = context.pathParam("table"); QTableMetaData table = qInstance.getTable(tableName); String primaryKey = context.pathParam("primaryKey"); - QueryInput queryInput = new QueryInput(qInstance); + GetInput getInput = new GetInput(qInstance); - setupSession(context, queryInput); - queryInput.setTableName(tableName); - queryInput.setShouldGenerateDisplayValues(true); - queryInput.setShouldTranslatePossibleValues(true); + setupSession(context, getInput); + getInput.setTableName(tableName); + getInput.setShouldGenerateDisplayValues(true); + getInput.setShouldTranslatePossibleValues(true); // todo - validate that the primary key is of the proper type (e.g,. not a string for an id field) // and throw a 400-series error (tell the user bad-request), rather than, we're doing a 500 (server error) - /////////////////////////////////////////////////////// - // setup a filter for the primaryKey = the path-pram // - /////////////////////////////////////////////////////// - queryInput.setFilter(new QQueryFilter() - .withCriteria(new QFilterCriteria() - .withFieldName(table.getPrimaryKeyField()) - .withOperator(QCriteriaOperator.EQUALS) - .withValues(List.of(primaryKey)))); + getInput.setPrimaryKey(primaryKey); - QueryAction queryAction = new QueryAction(); - QueryOutput queryOutput = queryAction.execute(queryInput); + GetAction getAction = new GetAction(); + GetOutput getOutput = getAction.execute(getInput); /////////////////////////////////////////////////////// // throw a not found error if the record isn't found // /////////////////////////////////////////////////////// - if(queryOutput.getRecords().isEmpty()) + if(getOutput.getRecord() == null) { throw (new QNotFoundException("Could not find " + table.getLabel() + " with " + table.getFields().get(table.getPrimaryKeyField()).getLabel() + " of " + primaryKey)); } - context.result(JsonUtils.toJson(queryOutput.getRecords().get(0))); + context.result(JsonUtils.toJson(getOutput.getRecord())); } catch(Exception e) { @@ -938,6 +959,211 @@ public class QJavalinImplementation + /******************************************************************************* + ** + *******************************************************************************/ + private static void getRecordDeveloperMode(Context context) + { + try + { + String tableName = context.pathParam("table"); + QTableMetaData table = qInstance.getTable(tableName); + String primaryKey = context.pathParam("primaryKey"); + GetInput getInput = new GetInput(qInstance); + + setupSession(context, getInput); + getInput.setTableName(tableName); + getInput.setShouldGenerateDisplayValues(true); + getInput.setShouldTranslatePossibleValues(true); + + // todo - validate that the primary key is of the proper type (e.g,. not a string for an id field) + // and throw a 400-series error (tell the user bad-request), rather than, we're doing a 500 (server error) + + getInput.setPrimaryKey(primaryKey); + + GetAction getAction = new GetAction(); + GetOutput getOutput = getAction.execute(getInput); + + /////////////////////////////////////////////////////// + // throw a not found error if the record isn't found // + /////////////////////////////////////////////////////// + QRecord record = getOutput.getRecord(); + if(record == null) + { + throw (new QNotFoundException("Could not find " + table.getLabel() + " with " + + table.getFields().get(table.getPrimaryKeyField()).getLabel() + " of " + primaryKey)); + } + + Map rs = new HashMap<>(); + rs.put("record", record); + + ArrayList> associatedScripts = new ArrayList<>(); + rs.put("associatedScripts", associatedScripts); + + /////////////////////////////////////////////////////// + // process each associated script type for the table // + /////////////////////////////////////////////////////// + for(AssociatedScript associatedScript : CollectionUtils.nonNullList(table.getAssociatedScripts())) + { + HashMap thisScriptData = new HashMap<>(); + associatedScripts.add(thisScriptData); + thisScriptData.put("associatedScript", associatedScript); + + String fieldName = associatedScript.getFieldName(); + Serializable scriptId = record.getValue(fieldName); + if(scriptId != null) + { + GetInput getScriptInput = new GetInput(qInstance); + setupSession(context, getScriptInput); + getScriptInput.setTableName("script"); + getScriptInput.setPrimaryKey(scriptId); + GetOutput getScriptOutput = new GetAction().execute(getScriptInput); + if(getScriptOutput.getRecord() != null) + { + thisScriptData.put("script", getScriptOutput.getRecord()); + + QueryInput queryInput = new QueryInput(qInstance); + setupSession(context, queryInput); + queryInput.setTableName("scriptRevision"); + queryInput.setFilter(new QQueryFilter() + .withCriteria(new QFilterCriteria("scriptId", QCriteriaOperator.EQUALS, List.of(getScriptOutput.getRecord().getValue("id")))) + .withOrderBy(new QFilterOrderBy("id", false)) + ); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + thisScriptData.put("scriptRevisions", new ArrayList<>(queryOutput.getRecords())); + } + } + } + + context.result(JsonUtils.toJson(rs)); + } + catch(Exception e) + { + handleException(context, e); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static void getAssociatedScriptLogs(Context context) + { + try + { + String scriptRevisionId = context.pathParam("scriptRevisionId"); + + QueryInput queryInput = new QueryInput(qInstance); + setupSession(context, queryInput); + queryInput.setTableName("scriptLog"); + queryInput.setFilter(new QQueryFilter() + .withCriteria(new QFilterCriteria("scriptRevisionId", QCriteriaOperator.EQUALS, List.of(scriptRevisionId))) + .withOrderBy(new QFilterOrderBy("id", false)) + ); + queryInput.setLimit(100); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + + if(CollectionUtils.nullSafeHasContents(queryOutput.getRecords())) + { + GeneralProcessUtils.addForeignRecordsListToRecordList(queryInput, queryOutput.getRecords(), "id", "scriptLogLine", "scriptLogId"); + } + + Map rs = new HashMap<>(); + rs.put("scriptLogRecords", new ArrayList<>(queryOutput.getRecords())); + + context.result(JsonUtils.toJson(rs)); + } + catch(Exception e) + { + handleException(context, e); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static void storeRecordAssociatedScript(Context context) + { + try + { + StoreAssociatedScriptInput input = new StoreAssociatedScriptInput(qInstance); + setupSession(context, input); + input.setCode(context.formParam("contents")); + input.setCommitMessage(context.formParam("commitMessage")); + input.setFieldName(context.pathParam("fieldName")); + input.setTableName(context.pathParam("table")); + input.setRecordPrimaryKey(context.pathParam("primaryKey")); + + StoreAssociatedScriptOutput output = new StoreAssociatedScriptOutput(); + + StoreAssociatedScriptAction storeAssociatedScriptAction = new StoreAssociatedScriptAction(); + storeAssociatedScriptAction.run(input, output); + + context.result(JsonUtils.toJson("OK")); + } + catch(Exception e) + { + handleException(context, e); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static void testAssociatedScript(Context context) + { + try + { + TestScriptInput input = new TestScriptInput(qInstance); + setupSession(context, input); + input.setTableName(context.pathParam("table")); + input.setRecordPrimaryKey(context.pathParam("primaryKey")); + // context.pathParam("fieldName"); + Map inputValues = new HashMap<>(); + input.setInputValues(inputValues); + + for(Map.Entry> entry : context.formParamMap().entrySet()) + { + String key = entry.getKey(); + String value = entry.getValue().get(0); + + if(key.equals("code")) + { + input.setCode(value); + } + else if(key.equals("scriptTypeId")) + { + input.setScriptTypeId(ValueUtils.getValueAsInteger(value)); + } + else + { + inputValues.put(key, value); + } + } + + TestScriptOutput output = new TestScriptOutput(); + + new TestScriptAction().run(input, output); + + ////////////////////////////////////////////////////////////// + // todo - output to frontend - then add assertions in test. // + ////////////////////////////////////////////////////////////// + + context.result(JsonUtils.toJson("OK")); + } + catch(Exception e) + { + handleException(context, e); + } + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java index 07a37be7..df3bb744 100644 --- a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java +++ b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java @@ -26,9 +26,23 @@ import java.io.Serializable; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.HashMap; +import java.util.List; import java.util.Map; +import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; +import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; +import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction; +import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormat; +import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; +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.actions.tables.query.QueryInput; +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.dashboard.widgets.WidgetType; +import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.model.session.QSession; import com.kingsrook.qqq.backend.core.utils.JsonUtils; import kong.unirest.HttpResponse; import kong.unirest.Unirest; @@ -68,7 +82,7 @@ class QJavalinImplementationTest extends QJavalinTestBase JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); assertTrue(jsonObject.has("tables")); JSONObject tables = jsonObject.getJSONObject("tables"); - assertEquals(1, tables.length()); + assertEquals(6, tables.length()); // person + 5 script tables JSONObject personTable = tables.getJSONObject("person"); assertTrue(personTable.has("name")); assertEquals("person", personTable.getString("name")); @@ -616,4 +630,135 @@ class QJavalinImplementationTest extends QJavalinTestBase assertEquals(5, jsonObject.getJSONArray("options").getJSONObject(1).getInt("id")); } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testGetRecordDeveloperMode() throws QException + { + UpdateInput updateInput = new UpdateInput(TestUtils.defineInstance()); + updateInput.setSession(new QSession()); + updateInput.setTableName("person"); + updateInput.setRecords(List.of(new QRecord().withValue("id", 1).withValue("testScriptId", 47))); + new UpdateAction().execute(updateInput); + + InsertInput insertInput = new InsertInput(TestUtils.defineInstance()); + insertInput.setSession(new QSession()); + insertInput.setTableName("script"); + insertInput.setRecords(List.of(new QRecord().withValue("id", 47).withValue("currentScriptRevisionId", 100))); + new InsertAction().execute(insertInput); + + insertInput.setTableName("scriptRevision"); + insertInput.setRecords(List.of(new QRecord().withValue("id", 1000).withValue("scriptId", 47).withValue("content", "var i;"))); + new InsertAction().execute(insertInput); + + HttpResponse response = Unirest.get(BASE_URL + "/data/person/1/developer").asString(); + assertEquals(200, response.getStatus()); + JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); + System.out.println(jsonObject.toString(3)); + assertNotNull(jsonObject); + assertNotNull(jsonObject.getJSONObject("record")); + assertEquals("Darin", jsonObject.getJSONObject("record").getJSONObject("values").getString("firstName")); + assertEquals("Darin", jsonObject.getJSONObject("record").getJSONObject("displayValues").getString("firstName")); + assertNotNull(jsonObject.getJSONArray("associatedScripts")); + assertNotNull(jsonObject.getJSONArray("associatedScripts").getJSONObject(0)); + assertNotNull(jsonObject.getJSONArray("associatedScripts").getJSONObject(0).getJSONArray("scriptRevisions")); + assertEquals("var i;", jsonObject.getJSONArray("associatedScripts").getJSONObject(0).getJSONArray("scriptRevisions").getJSONObject(0).getJSONObject("values").getString("content")); + assertNotNull(jsonObject.getJSONArray("associatedScripts").getJSONObject(0).getJSONObject("script")); + assertEquals(100, jsonObject.getJSONArray("associatedScripts").getJSONObject(0).getJSONObject("script").getJSONObject("values").getInt("currentScriptRevisionId")); + assertNotNull(jsonObject.getJSONArray("associatedScripts").getJSONObject(0).getJSONObject("associatedScript")); + assertEquals("testScriptId", jsonObject.getJSONArray("associatedScripts").getJSONObject(0).getJSONObject("associatedScript").getString("fieldName")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testStoreRecordAssociatedScript() throws QException + { + InsertInput insertInput = new InsertInput(TestUtils.defineInstance()); + insertInput.setSession(new QSession()); + insertInput.setTableName("scriptType"); + insertInput.setRecords(List.of(new QRecord().withValue("id", 1).withValue("name", "Test"))); + new InsertAction().execute(insertInput); + + HttpResponse response = Unirest.post(BASE_URL + "/data/person/1/developer/associatedScript/testScriptId") + .field("contents", "var j = 0;") + .field("commitMessage", "Javalin Commit") + .asString(); + + QueryInput queryInput = new QueryInput(TestUtils.defineInstance()); + queryInput.setSession(new QSession()); + queryInput.setTableName("scriptRevision"); + queryInput.setFilter(new QQueryFilter() + .withCriteria(new QFilterCriteria("contents", QCriteriaOperator.EQUALS, List.of("var j = 0;"))) + .withCriteria(new QFilterCriteria("commitMessage", QCriteriaOperator.EQUALS, List.of("Javalin Commit"))) + ); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + assertEquals(1, queryOutput.getRecords().size()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testTestAssociatedScript() throws QException + { + InsertInput insertInput = new InsertInput(TestUtils.defineInstance()); + insertInput.setSession(new QSession()); + insertInput.setTableName("scriptType"); + insertInput.setRecords(List.of(new QRecord().withValue("id", 1).withValue("name", "Test"))); + new InsertAction().execute(insertInput); + + HttpResponse response = Unirest.post(BASE_URL + "/data/person/1/developer/associatedScript/testScriptId/test") + .field("code", "var j = 0;") + .field("scriptTypeId", "1") + .field("x", "47") + .asString(); + + ///////////////////////////////////////// + // todo - assertions after implemented // + ///////////////////////////////////////// + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testGetAssociatedScriptLogs() throws QException + { + InsertInput insertInput = new InsertInput(TestUtils.defineInstance()); + insertInput.setSession(new QSession()); + insertInput.setTableName("scriptLog"); + insertInput.setRecords(List.of(new QRecord().withValue("id", 1).withValue("output", "testOutput").withValue("scriptRevisionId", 100))); + new InsertAction().execute(insertInput); + + insertInput.setTableName("scriptLogLine"); + insertInput.setRecords(List.of( + new QRecord().withValue("scriptLogId", 1).withValue("text", "line one"), + new QRecord().withValue("scriptLogId", 1).withValue("text", "line two") + )); + new InsertAction().execute(insertInput); + + HttpResponse response = Unirest.get(BASE_URL + "/data/person/1/developer/associatedScript/testScriptId/100/logs").asString(); + assertEquals(200, response.getStatus()); + JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); + assertNotNull(jsonObject.getJSONArray("scriptLogRecords")); + assertEquals(1, jsonObject.getJSONArray("scriptLogRecords").length()); + assertNotNull(jsonObject.getJSONArray("scriptLogRecords").getJSONObject(0).getJSONObject("values")); + assertEquals("testOutput", jsonObject.getJSONArray("scriptLogRecords").getJSONObject(0).getJSONObject("values").getString("output")); + assertNotNull(jsonObject.getJSONArray("scriptLogRecords").getJSONObject(0).getJSONObject("values").getJSONArray("scriptLogLine")); + assertEquals(2, jsonObject.getJSONArray("scriptLogRecords").getJSONObject(0).getJSONObject("values").getJSONArray("scriptLogLine").length()); + assertEquals("line one", jsonObject.getJSONArray("scriptLogRecords").getJSONObject(0).getJSONObject("values").getJSONArray("scriptLogLine").getJSONObject(0).getJSONObject("values").getString("text")); + assertEquals("line two", jsonObject.getJSONArray("scriptLogRecords").getJSONObject(0).getJSONObject("values").getJSONArray("scriptLogLine").getJSONObject(1).getJSONObject("values").getString("text")); + } + } 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 98071fc7..89da7217 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 @@ -31,6 +31,7 @@ import com.kingsrook.qqq.backend.core.exceptions.QValueException; 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.metadata.QAuthenticationType; +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.code.QCodeReference; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType; @@ -47,7 +48,9 @@ import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionInputMet import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionOutputMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QRecordListMetaData; +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.scripts.ScriptsMetaDataProvider; import com.kingsrook.qqq.backend.core.modules.authentication.metadata.QAuthenticationMetaData; import com.kingsrook.qqq.backend.core.processes.implementations.mock.MockBackendStep; import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager; @@ -84,7 +87,7 @@ public class TestUtils public static void primeTestDatabase() throws Exception { ConnectionManager connectionManager = new ConnectionManager(); - Connection connection = connectionManager.getConnection(TestUtils.defineBackend()); + Connection connection = connectionManager.getConnection(TestUtils.defineDefaultH2Backend()); InputStream primeTestDatabaseSqlStream = TestUtils.class.getResourceAsStream("/prime-test-database.sql"); assertNotNull(primeTestDatabaseSqlStream); List lines = (List) IOUtils.readLines(primeTestDatabaseSqlStream); @@ -105,7 +108,7 @@ public class TestUtils public static void runTestSql(String sql, QueryManager.ResultSetProcessor resultSetProcessor) throws Exception { ConnectionManager connectionManager = new ConnectionManager(); - Connection connection = connectionManager.getConnection(defineBackend()); + Connection connection = connectionManager.getConnection(defineDefaultH2Backend()); QueryManager.executeStatement(connection, sql, resultSetProcessor); } @@ -119,7 +122,7 @@ public class TestUtils { QInstance qInstance = new QInstance(); qInstance.setAuthentication(defineAuthentication()); - qInstance.addBackend(defineBackend()); + qInstance.addBackend(defineDefaultH2Backend()); qInstance.addTable(defineTablePerson()); qInstance.addProcess(defineProcessGreetPeople()); qInstance.addProcess(defineProcessGreetPeopleInteractive()); @@ -128,6 +131,17 @@ public class TestUtils qInstance.addProcess(defineProcessSimpleThrow()); qInstance.addPossibleValueSource(definePossibleValueSourcePerson()); defineWidgets(qInstance); + + qInstance.addBackend(defineMemoryBackend()); + try + { + new ScriptsMetaDataProvider().defineAll(qInstance, defineMemoryBackend().getName(), null); + } + catch(Exception e) + { + throw new IllegalStateException("Error adding script tables to instance"); + } + return (qInstance); } @@ -162,7 +176,7 @@ public class TestUtils ** Define the h2 rdbms backend ** *******************************************************************************/ - public static RDBMSBackendMetaData defineBackend() + public static RDBMSBackendMetaData defineDefaultH2Backend() { RDBMSBackendMetaData rdbmsBackendMetaData = new RDBMSBackendMetaData() .withVendor("h2") @@ -176,6 +190,19 @@ public class TestUtils + /******************************************************************************* + ** Define the memory-only backend + ** + *******************************************************************************/ + public static QBackendMetaData defineMemoryBackend() + { + return new QBackendMetaData() + .withBackendType("memory") + .withName("memory"); + } + + + /******************************************************************************* ** Define the person table ** @@ -187,7 +214,7 @@ public class TestUtils .withLabel("Person") .withRecordLabelFormat("%s %s") .withRecordLabelFields("firstName", "lastName") - .withBackendName(defineBackend().getName()) + .withBackendName(defineDefaultH2Backend().getName()) .withPrimaryKeyField("id") .withField(new QFieldMetaData("id", QFieldType.INTEGER)) .withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME).withBackendName("create_date")) @@ -196,7 +223,11 @@ public class TestUtils .withField(new QFieldMetaData("lastName", QFieldType.STRING).withBackendName("last_name")) .withField(new QFieldMetaData("birthDate", QFieldType.DATE).withBackendName("birth_date")) .withField(new QFieldMetaData("partnerPersonId", QFieldType.INTEGER).withBackendName("partner_person_id").withPossibleValueSourceName("person")) - .withField(new QFieldMetaData("email", QFieldType.STRING)); + .withField(new QFieldMetaData("email", QFieldType.STRING)) + .withField(new QFieldMetaData("testScriptId", QFieldType.INTEGER).withBackendName("test_script_id")) + .withAssociatedScript(new AssociatedScript() + .withFieldName("testScriptId") + .withScriptTypeId(1)); } diff --git a/qqq-middleware-javalin/src/test/resources/prime-test-database.sql b/qqq-middleware-javalin/src/test/resources/prime-test-database.sql index 22e88d10..c0a67766 100644 --- a/qqq-middleware-javalin/src/test/resources/prime-test-database.sql +++ b/qqq-middleware-javalin/src/test/resources/prime-test-database.sql @@ -30,7 +30,8 @@ CREATE TABLE person last_name VARCHAR(80) NOT NULL, birth_date DATE, email VARCHAR(250) NOT NULL, - partner_person_id INT + partner_person_id INT, + test_script_id INT ); INSERT INTO person (id, first_name, last_name, birth_date, email) VALUES (1, 'Darin', 'Kelkhoff', '1980-05-31', 'darin.kelkhoff@gmail.com');