From 9072ce242640f49d77f8f47389e6572fc017c741 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Mon, 10 Feb 2025 09:37:59 -0600 Subject: [PATCH 1/4] Initial implementation of process tracers --- .../actions/processes/RunProcessAction.java | 168 ++++++++++++++++++ .../processes/RunBackendStepInput.java | 61 ++++++- .../metadata/processes/QProcessMetaData.java | 34 ++++ .../tracing/LoggingProcessTracer.java | 154 ++++++++++++++++ .../processes/tracing/NoopProcessTracer.java | 107 +++++++++++ .../tracing/ProcessTracerInterface.java | 77 ++++++++ .../tracing/ProcessTracerMessage.java | 85 +++++++++ .../tracing/LoggingProcessTracerTest.java | 93 ++++++++++ .../tracing/NoopProcessTracerTest.java | 61 +++++++ 9 files changed, 839 insertions(+), 1 deletion(-) create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/tracing/LoggingProcessTracer.java create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/tracing/NoopProcessTracer.java create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/tracing/ProcessTracerInterface.java create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/tracing/ProcessTracerMessage.java create mode 100644 qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/tracing/LoggingProcessTracerTest.java create mode 100644 qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/tracing/NoopProcessTracerTest.java 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 a595b4e9..b6236fef 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 @@ -32,6 +32,7 @@ import java.util.Objects; import java.util.Optional; import java.util.UUID; import com.kingsrook.qqq.backend.core.actions.ActionHelper; +import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader; import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.NoCodeWidgetRenderer; import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; @@ -53,6 +54,7 @@ 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.QBackendMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.NoCodeWidgetFrontendComponentMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData; @@ -63,6 +65,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.processes.QStateMachineStep import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData; import com.kingsrook.qqq.backend.core.model.session.QSession; import com.kingsrook.qqq.backend.core.processes.implementations.basepull.BasepullConfiguration; +import com.kingsrook.qqq.backend.core.processes.tracing.ProcessTracerInterface; import com.kingsrook.qqq.backend.core.state.InMemoryStateProvider; import com.kingsrook.qqq.backend.core.state.StateProviderInterface; import com.kingsrook.qqq.backend.core.state.StateType; @@ -71,6 +74,7 @@ import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.core.utils.ValueUtils; import org.apache.commons.lang.BooleanUtils; +import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; /******************************************************************************* @@ -87,12 +91,16 @@ public class RunProcessAction public static final String BASEPULL_TIMESTAMP_FIELD = "basepullTimestampField"; public static final String BASEPULL_CONFIGURATION = "basepullConfiguration"; + public static final String PROCESS_TRACER_CODE_REFERENCE_FIELD = "processTracerCodeReference"; + //////////////////////////////////////////////////////////////////////////////////////////////// // 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"; public static final String BASEPULL_DID_QUERY_USING_TIMESTAMP_FIELD = "basepullDidQueryUsingTimestamp"; + private ProcessTracerInterface processTracer; + /******************************************************************************* @@ -110,6 +118,8 @@ public class RunProcessAction RunProcessOutput runProcessOutput = new RunProcessOutput(); + traceStartOrResume(runProcessInput, process); + ////////////////////////////////////////////////////////// // generate a UUID for the process, if one wasn't given // ////////////////////////////////////////////////////////// @@ -166,6 +176,7 @@ public class RunProcessAction //////////////////////////////////////////////////////////// // upon exception (e.g., one thrown by a step), throw it. // //////////////////////////////////////////////////////////// + traceBreakOrFinish(runProcessInput, runProcessOutput, qe); throw (qe); } catch(Exception e) @@ -173,6 +184,7 @@ public class RunProcessAction //////////////////////////////////////////////////////////// // upon exception (e.g., one thrown by a step), throw it. // //////////////////////////////////////////////////////////// + traceBreakOrFinish(runProcessInput, runProcessOutput, e); throw (new QException("Error running process", e)); } finally @@ -183,6 +195,8 @@ public class RunProcessAction runProcessOutput.setProcessState(processState); } + traceBreakOrFinish(runProcessInput, runProcessOutput, null); + return (runProcessOutput); } @@ -630,6 +644,7 @@ public class RunProcessAction runBackendStepInput.setCallback(runProcessInput.getCallback()); runBackendStepInput.setFrontendStepBehavior(runProcessInput.getFrontendStepBehavior()); runBackendStepInput.setAsyncJobCallback(runProcessInput.getAsyncJobCallback()); + runBackendStepInput.setProcessTracer(processTracer); runBackendStepInput.setTableName(process.getTableName()); if(!StringUtils.hasContent(runBackendStepInput.getTableName())) @@ -651,9 +666,13 @@ public class RunProcessAction runBackendStepInput.setBasepullLastRunTime((Instant) runProcessInput.getValues().get(BASEPULL_LAST_RUNTIME_KEY)); } + traceStepStart(runBackendStepInput); + RunBackendStepOutput runBackendStepOutput = new RunBackendStepAction().execute(runBackendStepInput); storeState(stateKey, runBackendStepOutput.getProcessState()); + traceStepFinish(runBackendStepInput, runBackendStepOutput); + if(runBackendStepOutput.getException() != null) { runProcessOutput.setException(runBackendStepOutput.getException()); @@ -931,4 +950,153 @@ public class RunProcessAction runProcessInput.getValues().put(BASEPULL_TIMESTAMP_FIELD, basepullConfiguration.getTimestampField()); runProcessInput.getValues().put(BASEPULL_CONFIGURATION, basepullConfiguration); } + + + + /*************************************************************************** + ** + ***************************************************************************/ + private void setupProcessTracer(RunProcessInput runProcessInput, QProcessMetaData process) + { + try + { + if(process.getProcessTracerCodeReference() != null) + { + processTracer = QCodeLoader.getAdHoc(ProcessTracerInterface.class, process.getProcessTracerCodeReference()); + } + + Serializable processTracerCodeReference = runProcessInput.getValue(PROCESS_TRACER_CODE_REFERENCE_FIELD); + if(processTracerCodeReference != null) + { + if(processTracerCodeReference instanceof QCodeReference codeReference) + { + processTracer = QCodeLoader.getAdHoc(ProcessTracerInterface.class, codeReference); + } + } + } + catch(Exception e) + { + LOG.warn("Error setting up processTracer", e, logPair("processName", runProcessInput.getProcessName())); + } + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + private void traceStartOrResume(RunProcessInput runProcessInput, QProcessMetaData process) + { + setupProcessTracer(runProcessInput, process); + + try + { + if(processTracer != null) + { + if(StringUtils.hasContent(runProcessInput.getStartAfterStep()) || StringUtils.hasContent(runProcessInput.getStartAtStep())) + { + processTracer.handleProcessResume(runProcessInput); + } + else + { + processTracer.handleProcessStart(runProcessInput); + } + } + } + catch(Exception e) + { + LOG.info("Error in traceStart", e, logPair("processName", runProcessInput.getProcessName())); + } + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + private void traceBreakOrFinish(RunProcessInput runProcessInput, RunProcessOutput runProcessOutput, Exception processException) + { + try + { + if(processTracer != null) + { + ProcessState processState = runProcessOutput.getProcessState(); + boolean isBreak = true; + + ///////////////////////////////////////////////////////////// + // if there's no next step, that means the process is done // + ///////////////////////////////////////////////////////////// + if(processState.getNextStepName().isEmpty()) + { + isBreak = false; + } + else + { + ///////////////////////////////////////////////////////////////// + // or if the next step is the last index, then we're also done // + ///////////////////////////////////////////////////////////////// + String nextStepName = processState.getNextStepName().get(); + int nextStepIndex = processState.getStepList().indexOf(nextStepName); + if(nextStepIndex == processState.getStepList().size() - 1) + { + isBreak = false; + } + } + + if(isBreak) + { + processTracer.handleProcessBreak(runProcessInput, runProcessOutput, processException); + } + else + { + processTracer.handleProcessFinish(runProcessInput, runProcessOutput, processException); + } + } + } + catch(Exception e) + { + LOG.info("Error in traceProcessFinish", e, logPair("processName", runProcessInput.getProcessName())); + } + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + private void traceStepStart(RunBackendStepInput runBackendStepInput) + { + try + { + if(processTracer != null) + { + processTracer.handleStepStart(runBackendStepInput); + } + } + catch(Exception e) + { + LOG.info("Error in traceStepFinish", e, logPair("processName", runBackendStepInput.getProcessName()), logPair("stepName", runBackendStepInput.getStepName())); + } + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + private void traceStepFinish(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) + { + try + { + if(processTracer != null) + { + processTracer.handleStepFinish(runBackendStepInput, runBackendStepOutput); + } + } + catch(Exception e) + { + LOG.info("Error in traceStepFinish", e, logPair("processName", runBackendStepInput.getProcessName()), logPair("stepName", runBackendStepInput.getStepName())); + } + } + } 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 81ff1d77..17b3e608 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 @@ -27,17 +27,22 @@ import java.time.Instant; import java.time.LocalDate; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.UUID; import com.kingsrook.qqq.backend.core.actions.async.AsyncJobCallback; import com.kingsrook.qqq.backend.core.actions.async.AsyncJobStatus; import com.kingsrook.qqq.backend.core.actions.async.NonPersistedAsyncJobCallback; import com.kingsrook.qqq.backend.core.actions.processes.QProcessCallback; import com.kingsrook.qqq.backend.core.context.QContext; +import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput; import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.processes.tracing.ProcessTracerInterface; +import com.kingsrook.qqq.backend.core.processes.tracing.ProcessTracerMessage; import com.kingsrook.qqq.backend.core.utils.ValueUtils; +import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; /******************************************************************************* @@ -46,6 +51,8 @@ import com.kingsrook.qqq.backend.core.utils.ValueUtils; *******************************************************************************/ public class RunBackendStepInput extends AbstractActionInput { + private static final QLogger LOG = QLogger.getLogger(RunBackendStepInput.class); + private ProcessState processState; private String processName; private String tableName; @@ -55,12 +62,13 @@ public class RunBackendStepInput extends AbstractActionInput private RunProcessInput.FrontendStepBehavior frontendStepBehavior; private Instant basepullLastRunTime; + private ProcessTracerInterface processTracer; + //////////////////////////////////////////////////////////////////////////// // note - new fields should generally be added in method: cloneFieldsInto // //////////////////////////////////////////////////////////////////////////// - /******************************************************************************* ** *******************************************************************************/ @@ -96,6 +104,7 @@ public class RunBackendStepInput extends AbstractActionInput target.setAsyncJobCallback(getAsyncJobCallback()); target.setFrontendStepBehavior(getFrontendStepBehavior()); target.setValues(getValues()); + target.setProcessTracer(getProcessTracer().orElse(null)); } @@ -535,4 +544,54 @@ public class RunBackendStepInput extends AbstractActionInput return (this); } + + + /******************************************************************************* + ** Setter for processTracer + *******************************************************************************/ + public void setProcessTracer(ProcessTracerInterface processTracer) + { + this.processTracer = processTracer; + } + + + + /******************************************************************************* + ** Fluent setter for processTracer + *******************************************************************************/ + public RunBackendStepInput withProcessTracer(ProcessTracerInterface processTracer) + { + this.processTracer = processTracer; + return (this); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + public Optional getProcessTracer() + { + return Optional.ofNullable(processTracer); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + public void traceMessage(ProcessTracerMessage message) + { + if(processTracer != null) + { + try + { + processTracer.handleMessage(this, message); + } + catch(Exception e) + { + LOG.warn("Error tracing message", e, logPair("message", message)); + } + } + } } 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 8bb459ef..582b36e2 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 @@ -31,6 +31,7 @@ import java.util.Set; import com.fasterxml.jackson.annotation.JsonIgnore; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.TopLevelMetaDataInterface; +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.layout.QAppChildMetaData; import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon; @@ -70,6 +71,8 @@ public class QProcessMetaData implements QAppChildMetaData, MetaDataWithPermissi private VariantRunStrategy variantRunStrategy; private String variantBackend; + private QCodeReference processTracerCodeReference; + private Map supplementalMetaData; @@ -866,4 +869,35 @@ public class QProcessMetaData implements QAppChildMetaData, MetaDataWithPermissi return (this); } + + /******************************************************************************* + ** Getter for processTracerCodeReference + *******************************************************************************/ + public QCodeReference getProcessTracerCodeReference() + { + return (this.processTracerCodeReference); + } + + + + /******************************************************************************* + ** Setter for processTracerCodeReference + *******************************************************************************/ + public void setProcessTracerCodeReference(QCodeReference processTracerCodeReference) + { + this.processTracerCodeReference = processTracerCodeReference; + } + + + + /******************************************************************************* + ** Fluent setter for processTracerCodeReference + *******************************************************************************/ + public QProcessMetaData withProcessTracerCodeReference(QCodeReference processTracerCodeReference) + { + this.processTracerCodeReference = processTracerCodeReference; + return (this); + } + + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/tracing/LoggingProcessTracer.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/tracing/LoggingProcessTracer.java new file mode 100644 index 00000000..afaa80fe --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/tracing/LoggingProcessTracer.java @@ -0,0 +1,154 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2025. 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.tracing; + + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import com.kingsrook.qqq.backend.core.logging.LogPair; +import com.kingsrook.qqq.backend.core.logging.QLogger; +import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLine; +import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLineInterface; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput; +import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.StreamedETLWithFrontendProcess; +import com.kingsrook.qqq.backend.core.utils.StringUtils; +import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; + + +/******************************************************************************* + ** Implementation of ProcessTracerInterface that writes messages to the Logger. + *******************************************************************************/ +public class LoggingProcessTracer implements ProcessTracerInterface +{ + private static final QLogger LOG = QLogger.getLogger(LoggingProcessTracer.class); + + private long startMillis; + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public void handleProcessStart(RunProcessInput runProcessInput) + { + startMillis = System.currentTimeMillis(); + LOG.info("Starting process", logPair("name", runProcessInput.getProcessName()), logPair("uuid", runProcessInput.getProcessUUID())); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public void handleProcessResume(RunProcessInput runProcessInput) + { + String atOrAfter = "after"; + String atOrAfterStep = runProcessInput.getStartAfterStep(); + if(StringUtils.hasContent(runProcessInput.getStartAtStep())) + { + atOrAfter = "at"; + atOrAfterStep = runProcessInput.getStartAtStep(); + } + LOG.info("Resuming process", logPair("name", runProcessInput.getProcessName()), logPair("uuid", runProcessInput.getProcessUUID()), logPair(atOrAfter, atOrAfterStep)); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public void handleStepStart(RunBackendStepInput runBackendStepInput) + { + LOG.info("Starting process step", runBackendStepInput.getStepName()); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public void handleMessage(RunBackendStepInput runBackendStepInput, ProcessTracerMessage message) + { + LOG.info("Message from process", logPair("message", message.getMessage())); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public void handleStepFinish(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) + { + LOG.info("Finished process step", logPair("name", runBackendStepInput.getStepName())); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public void handleProcessBreak(RunProcessInput runProcessInput, RunProcessOutput runProcessOutput, Exception processException) + { + LOG.info("Breaking process", logPair("name", runProcessInput.getProcessName()), logPair("uuid", runProcessInput.getProcessUUID())); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public void handleProcessFinish(RunProcessInput runProcessInput, RunProcessOutput runProcessOutput, Exception processException) + { + long finishMillis = System.currentTimeMillis(); + + List summaryLogPairs = new ArrayList<>(); + Serializable processSummary = runProcessOutput.getValue(StreamedETLWithFrontendProcess.FIELD_PROCESS_SUMMARY); + if(processSummary instanceof List) + { + List processSummaryLines = (List) processSummary; + for(ProcessSummaryLineInterface processSummaryLineInterface : processSummaryLines) + { + if(processSummaryLineInterface instanceof ProcessSummaryLine processSummaryLine) + { + summaryLogPairs.add(logPair(String.valueOf(summaryLogPairs.size()), logPair("status", processSummaryLine.getStatus()), logPair("count", processSummaryLine.getCount()), logPair("message", processSummaryLine.getMessage()))); + } + else + { + summaryLogPairs.add(logPair(String.valueOf(summaryLogPairs.size()), logPair("message", processSummaryLineInterface.getMessage()))); + } + } + } + + LOG.info("Finished process", logPair("name", runProcessInput.getProcessName()), logPair("uuid", runProcessInput.getProcessUUID()), logPair("millis", finishMillis - startMillis), logPair("summary", summaryLogPairs)); + } +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/tracing/NoopProcessTracer.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/tracing/NoopProcessTracer.java new file mode 100644 index 00000000..aeee851a --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/tracing/NoopProcessTracer.java @@ -0,0 +1,107 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2025. 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.tracing; + + +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; + + +/******************************************************************************* + ** Implementation of ProcessTracerInterface that does nothing (no-op). + *******************************************************************************/ +public class NoopProcessTracer implements ProcessTracerInterface +{ + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public void handleProcessStart(RunProcessInput runProcessInput) + { + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public void handleProcessResume(RunProcessInput runProcessInput) + { + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public void handleStepStart(RunBackendStepInput runBackendStepInput) + { + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public void handleMessage(RunBackendStepInput runBackendStepInput, ProcessTracerMessage message) + { + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public void handleStepFinish(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) + { + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public void handleProcessBreak(RunProcessInput runProcessInput, RunProcessOutput runProcessOutput, Exception processException) + { + } + + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public void handleProcessFinish(RunProcessInput runProcessInput, RunProcessOutput runProcessOutput, Exception processException) + { + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/tracing/ProcessTracerInterface.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/tracing/ProcessTracerInterface.java new file mode 100644 index 00000000..e2d2ac84 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/tracing/ProcessTracerInterface.java @@ -0,0 +1,77 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2025. 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.tracing; + + +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; + + +/******************************************************************************* + ** Interface that can be plugged into the execution of a QProcess, that gets + ** callbacks from QQQ for events in the lifecycle of a process, which one may + ** wish to log or otherwise be aware of. + *******************************************************************************/ +public interface ProcessTracerInterface +{ + /*************************************************************************** + ** Called when a new process is started. + ***************************************************************************/ + void handleProcessStart(RunProcessInput runProcessInput); + + /*************************************************************************** + ** Called when a process is resumed, e.g., after a "break" occurs between + ** backend steps and frontend steps. + ***************************************************************************/ + void handleProcessResume(RunProcessInput runProcessInput); + + /*************************************************************************** + ** Called when a (backend) step is started. + ***************************************************************************/ + void handleStepStart(RunBackendStepInput runBackendStepInput); + + /*************************************************************************** + ** Called when the (application, custom) process step code itself decides to + ** trace something. We imagine various subclasses of ProcessTracerMessage + ** to be created, to communicate more specific data for the tracer implementation. + ***************************************************************************/ + void handleMessage(RunBackendStepInput runBackendStepInput, ProcessTracerMessage message); + + /*************************************************************************** + ** Called when a (backend) step finishes. + ***************************************************************************/ + void handleStepFinish(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput); + + /*************************************************************************** + ** Called when a process break occurs, e.g., between backend and frontend + ** steps (but only if there are no more backend steps in the queue). + ***************************************************************************/ + void handleProcessBreak(RunProcessInput runProcessInput, RunProcessOutput runProcessOutput, Exception processException); + + /*************************************************************************** + ** Called after the last (backend) step of a process. + ***************************************************************************/ + void handleProcessFinish(RunProcessInput runProcessInput, RunProcessOutput runProcessOutput, Exception processException); + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/tracing/ProcessTracerMessage.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/tracing/ProcessTracerMessage.java new file mode 100644 index 00000000..df2dd558 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/tracing/ProcessTracerMessage.java @@ -0,0 +1,85 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2025. 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.tracing; + + +/******************************************************************************* + ** Basic class that can be passed in to ProcessTracerInterface.handleMessage. + ** This class just provides for a string message. We anticipate subclasses + ** that may have more specific data, that specific tracer implementations may + ** be aware of. + *******************************************************************************/ +public class ProcessTracerMessage +{ + private String message; + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public ProcessTracerMessage() + { + } + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public ProcessTracerMessage(String message) + { + this.message = message; + } + + + + /******************************************************************************* + ** Getter for message + *******************************************************************************/ + public String getMessage() + { + return (this.message); + } + + + + /******************************************************************************* + ** Setter for message + *******************************************************************************/ + public void setMessage(String message) + { + this.message = message; + } + + + + /******************************************************************************* + ** Fluent setter for message + *******************************************************************************/ + public ProcessTracerMessage withMessage(String message) + { + this.message = message; + return (this); + } + +} diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/tracing/LoggingProcessTracerTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/tracing/LoggingProcessTracerTest.java new file mode 100644 index 00000000..ec3c8e60 --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/tracing/LoggingProcessTracerTest.java @@ -0,0 +1,93 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2025. 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.tracing; + + +import java.util.List; +import com.kingsrook.qqq.backend.core.BaseTest; +import com.kingsrook.qqq.backend.core.actions.processes.QProcessCallbackFactory; +import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.logging.QCollectingLogger; +import com.kingsrook.qqq.backend.core.logging.QLogger; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput; +import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; +import com.kingsrook.qqq.backend.core.utils.CollectionAssert; +import com.kingsrook.qqq.backend.core.utils.TestUtils; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertNotNull; + + +/******************************************************************************* + ** Unit test for LoggingProcessTracer + *******************************************************************************/ +class LoggingProcessTracerTest extends BaseTest +{ + /******************************************************************************* + ** + *******************************************************************************/ + @AfterEach + void afterEach() + { + QLogger.deactivateCollectingLoggerForClass(LoggingProcessTracer.class); + } + + + + /******************************************************************************* + ** this test is based on RunProcessTest#testBreakOnFrontendSteps + *******************************************************************************/ + @Test + void test() throws QException + { + ///////////////////////////////////////////////////// + // activate the tracer for this run of the process // + ///////////////////////////////////////////////////// + RunProcessInput input = new RunProcessInput(); + input.addValue(RunProcessAction.PROCESS_TRACER_CODE_REFERENCE_FIELD, new QCodeReference(LoggingProcessTracer.class)); + QCollectingLogger collectingLogger = QLogger.activateCollectingLoggerForClass(LoggingProcessTracer.class); + + input.setProcessName(TestUtils.PROCESS_NAME_GREET_PEOPLE_INTERACTIVE); + input.setFrontendStepBehavior(RunProcessInput.FrontendStepBehavior.BREAK); + input.setCallback(QProcessCallbackFactory.forRecord(new QRecord().withValue("id", 1))); + RunProcessOutput result0 = new RunProcessAction().execute(input); + assertNotNull(result0); + + CollectionAssert.assertThat(collectingLogger.getCollectedMessages()) + .matchesAll(List.of("Starting process", "Breaking process"), + (s, clm) -> clm.getMessageAsJSONObject().getString("message").equals(s)); + collectingLogger.clear(); + + /////////////////////////////////////////////////// + // now re-run (resume) to the end of the process // + /////////////////////////////////////////////////// + input.setStartAfterStep(result0.getProcessState().getNextStepName().get()); + RunProcessOutput result1 = new RunProcessAction().execute(input); + CollectionAssert.assertThat(collectingLogger.getCollectedMessages()) + .matchesAll(List.of("Resuming process", "Starting process step", "Finished process step", "Finished process"), + (s, clm) -> clm.getMessageAsJSONObject().getString("message").equals(s)); + } + +} \ No newline at end of file diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/tracing/NoopProcessTracerTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/tracing/NoopProcessTracerTest.java new file mode 100644 index 00000000..b46592aa --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/tracing/NoopProcessTracerTest.java @@ -0,0 +1,61 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2025. 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.tracing; + + +import com.kingsrook.qqq.backend.core.BaseTest; +import com.kingsrook.qqq.backend.core.actions.processes.QProcessCallbackFactory; +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.data.QRecord; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; +import com.kingsrook.qqq.backend.core.utils.TestUtils; +import org.junit.jupiter.api.Test; + + +/******************************************************************************* + ** Unit test for NoopProcessTracer ... kinda a BS test, but here to prevent + ** a missing-class for code coverage... + *******************************************************************************/ +class NoopProcessTracerTest extends BaseTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void test() throws QException + { + ////////////////////////////////////////////////////////// + // activate the noop tracer for this run of the process // + ////////////////////////////////////////////////////////// + RunProcessInput input = new RunProcessInput(); + input.addValue(RunProcessAction.PROCESS_TRACER_CODE_REFERENCE_FIELD, new QCodeReference(NoopProcessTracer.class)); + + input.setProcessName(TestUtils.PROCESS_NAME_GREET_PEOPLE_INTERACTIVE); + input.setFrontendStepBehavior(RunProcessInput.FrontendStepBehavior.BREAK); + input.setCallback(QProcessCallbackFactory.forRecord(new QRecord().withValue("id", 1))); + new RunProcessAction().execute(input); + } + +} \ No newline at end of file From eb8fa42fb877b51f860c3ef7a96e29b02bc47763 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Mon, 10 Feb 2025 09:52:06 -0600 Subject: [PATCH 2/4] Initial build of QQQProcess table - analog to QQQTable table, but for processes; refactoring of QQQTable record management into util class (out of QueryStatManager where it was originally used) --- .../tables/helpers/QueryStatManager.java | 60 +---- .../core/model/processes/QQQProcess.java | 230 ++++++++++++++++++ .../processes/QQQProcessTableManager.java | 92 +++++++ .../QQQProcessesMetaDataProvider.java | 134 ++++++++++ .../backend/core/model/tables/QQQTable.java | 4 +- .../model/tables/QQQTableTableManager.java | 92 +++++++ .../tables/QQQTablesMetaDataProvider.java | 2 +- .../processes/QQQProcessTableManagerTest.java | 95 ++++++++ .../tables/QQQTableTableManagerTest.java | 94 +++++++ 9 files changed, 746 insertions(+), 57 deletions(-) create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/processes/QQQProcess.java create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/processes/QQQProcessTableManager.java create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/processes/QQQProcessesMetaDataProvider.java create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/tables/QQQTableTableManager.java create mode 100644 qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/processes/QQQProcessTableManagerTest.java create mode 100644 qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/tables/QQQTableTableManagerTest.java diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/helpers/QueryStatManager.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/helpers/QueryStatManager.java index a2bdbe52..d7edce2d 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/helpers/QueryStatManager.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/helpers/QueryStatManager.java @@ -31,16 +31,12 @@ import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.function.Supplier; -import com.kingsrook.qqq.backend.core.actions.tables.GetAction; import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter; import com.kingsrook.qqq.backend.core.logging.QLogger; -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.QFilterCriteria; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; @@ -54,12 +50,10 @@ import com.kingsrook.qqq.backend.core.model.querystats.QueryStatCriteriaField; import com.kingsrook.qqq.backend.core.model.querystats.QueryStatJoinTable; import com.kingsrook.qqq.backend.core.model.querystats.QueryStatOrderByField; import com.kingsrook.qqq.backend.core.model.session.QSession; -import com.kingsrook.qqq.backend.core.model.tables.QQQTable; -import com.kingsrook.qqq.backend.core.model.tables.QQQTablesMetaDataProvider; +import com.kingsrook.qqq.backend.core.model.tables.QQQTableTableManager; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.PrefixedDefaultThreadFactory; import com.kingsrook.qqq.backend.core.utils.StringUtils; -import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder; import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; @@ -371,7 +365,7 @@ public class QueryStatManager ////////////////////// // set the table id // ////////////////////// - Integer qqqTableId = getQQQTableId(queryStat.getTableName()); + Integer qqqTableId = QQQTableTableManager.getQQQTableId(getInstance().qInstance, queryStat.getTableName()); queryStat.setQqqTableId(qqqTableId); ////////////////////////////// @@ -382,7 +376,7 @@ public class QueryStatManager List queryStatJoinTableList = new ArrayList<>(); for(String joinTableName : queryStat.getJoinTableNames()) { - queryStatJoinTableList.add(new QueryStatJoinTable().withQqqTableId(getQQQTableId(joinTableName))); + queryStatJoinTableList.add(new QueryStatJoinTable().withQqqTableId(QQQTableTableManager.getQQQTableId(getInstance().qInstance, joinTableName))); } queryStat.setQueryStatJoinTableList(queryStatJoinTableList); } @@ -460,7 +454,7 @@ public class QueryStatManager String[] parts = fieldName.split("\\."); if(parts.length > 1) { - queryStatCriteriaField.setQqqTableId(getQQQTableId(parts[0])); + queryStatCriteriaField.setQqqTableId(QQQTableTableManager.getQQQTableId(getInstance().qInstance, parts[0])); queryStatCriteriaField.setName(parts[1]); } } @@ -498,7 +492,7 @@ public class QueryStatManager String[] parts = fieldName.split("\\."); if(parts.length > 1) { - queryStatOrderByField.setQqqTableId(getQQQTableId(parts[0])); + queryStatOrderByField.setQqqTableId(QQQTableTableManager.getQQQTableId(getInstance().qInstance, parts[0])); queryStatOrderByField.setName(parts[1]); } } @@ -512,50 +506,6 @@ public class QueryStatManager } } } - - - - /******************************************************************************* - ** - *******************************************************************************/ - private static Integer getQQQTableId(String tableName) throws QException - { - ///////////////////////////// - // look in the cache table // - ///////////////////////////// - GetInput getInput = new GetInput(); - getInput.setTableName(QQQTablesMetaDataProvider.QQQ_TABLE_CACHE_TABLE_NAME); - getInput.setUniqueKey(MapBuilder.of("name", tableName)); - GetOutput getOutput = new GetAction().execute(getInput); - - //////////////////////// - // upon cache miss... // - //////////////////////// - if(getOutput.getRecord() == null) - { - QTableMetaData tableMetaData = getInstance().qInstance.getTable(tableName); - if(tableMetaData == null) - { - LOG.info("No such table", logPair("tableName", tableName)); - return (null); - } - - /////////////////////////////////////////////////////// - // insert the record (into the table, not the cache) // - /////////////////////////////////////////////////////// - InsertInput insertInput = new InsertInput(); - insertInput.setTableName(QQQTable.TABLE_NAME); - insertInput.setRecords(List.of(new QRecord().withValue("name", tableName).withValue("label", tableMetaData.getLabel()))); - InsertOutput insertOutput = new InsertAction().execute(insertInput); - - /////////////////////////////////// - // repeat the get from the cache // - /////////////////////////////////// - getOutput = new GetAction().execute(getInput); - } - - return getOutput.getRecord().getValueInteger("id"); - } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/processes/QQQProcess.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/processes/QQQProcess.java new file mode 100644 index 00000000..6702fca1 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/processes/QQQProcess.java @@ -0,0 +1,230 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2025. 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.processes; + + +import java.time.Instant; +import com.kingsrook.qqq.backend.core.model.data.QField; +import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.model.data.QRecordEntity; +import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior; + + +/******************************************************************************* + ** QRecord Entity for QQQProcess table - e.g., table that stores an id, name + ** and the label for all processes in the QQQ application. Useful as a foreign + ** key from other logging type tables. + *******************************************************************************/ +public class QQQProcess extends QRecordEntity +{ + public static final String TABLE_NAME = "qqqProcess"; + + @QField(isEditable = false) + private Integer id; + + @QField(isEditable = false) + private Instant createDate; + + @QField(isEditable = false) + private Instant modifyDate; + + @QField(maxLength = 250, valueTooLongBehavior = ValueTooLongBehavior.ERROR) + private String name; + + @QField(maxLength = 250, valueTooLongBehavior = ValueTooLongBehavior.TRUNCATE_ELLIPSIS) + private String label; + + + + /******************************************************************************* + ** Default constructor + *******************************************************************************/ + public QQQProcess() + { + } + + + + /******************************************************************************* + ** Constructor that takes a QRecord + *******************************************************************************/ + public QQQProcess(QRecord record) + { + populateFromQRecord(record); + } + + + + /******************************************************************************* + ** Getter for id + *******************************************************************************/ + public Integer getId() + { + return (this.id); + } + + + + /******************************************************************************* + ** Setter for id + *******************************************************************************/ + public void setId(Integer id) + { + this.id = id; + } + + + + /******************************************************************************* + ** Fluent setter for id + *******************************************************************************/ + public QQQProcess withId(Integer id) + { + this.id = id; + return (this); + } + + + + /******************************************************************************* + ** Getter for createDate + *******************************************************************************/ + public Instant getCreateDate() + { + return (this.createDate); + } + + + + /******************************************************************************* + ** Setter for createDate + *******************************************************************************/ + public void setCreateDate(Instant createDate) + { + this.createDate = createDate; + } + + + + /******************************************************************************* + ** Fluent setter for createDate + *******************************************************************************/ + public QQQProcess withCreateDate(Instant createDate) + { + this.createDate = createDate; + return (this); + } + + + + /******************************************************************************* + ** Getter for modifyDate + *******************************************************************************/ + public Instant getModifyDate() + { + return (this.modifyDate); + } + + + + /******************************************************************************* + ** Setter for modifyDate + *******************************************************************************/ + public void setModifyDate(Instant modifyDate) + { + this.modifyDate = modifyDate; + } + + + + /******************************************************************************* + ** Fluent setter for modifyDate + *******************************************************************************/ + public QQQProcess withModifyDate(Instant modifyDate) + { + this.modifyDate = modifyDate; + return (this); + } + + + + /******************************************************************************* + ** Getter for name + *******************************************************************************/ + public String getName() + { + return (this.name); + } + + + + /******************************************************************************* + ** Setter for name + *******************************************************************************/ + public void setName(String name) + { + this.name = name; + } + + + + /******************************************************************************* + ** Fluent setter for name + *******************************************************************************/ + public QQQProcess withName(String name) + { + this.name = name; + return (this); + } + + + + /******************************************************************************* + ** Getter for label + *******************************************************************************/ + public String getLabel() + { + return (this.label); + } + + + + /******************************************************************************* + ** Setter for label + *******************************************************************************/ + public void setLabel(String label) + { + this.label = label; + } + + + + /******************************************************************************* + ** Fluent setter for label + *******************************************************************************/ + public QQQProcess withLabel(String label) + { + this.label = label; + return (this); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/processes/QQQProcessTableManager.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/processes/QQQProcessTableManager.java new file mode 100644 index 00000000..b7d5c400 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/processes/QQQProcessTableManager.java @@ -0,0 +1,92 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2025. 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.processes; + + +import java.util.List; +import com.kingsrook.qqq.backend.core.actions.tables.GetAction; +import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.logging.QLogger; +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.data.QRecord; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; +import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder; +import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; + + +/******************************************************************************* + ** Utility class for accessing QQQProcess records (well, just their ids at this time) + ** Takes care of inserting upon a miss, and dealing with the cache table. + *******************************************************************************/ +public class QQQProcessTableManager +{ + private static final QLogger LOG = QLogger.getLogger(QQQProcessTableManager.class); + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static Integer getQQQProcessId(QInstance qInstance, String processName) throws QException + { + ///////////////////////////// + // look in the cache table // + ///////////////////////////// + GetInput getInput = new GetInput(); + getInput.setTableName(QQQProcessesMetaDataProvider.QQQ_PROCESS_CACHE_TABLE_NAME); + getInput.setUniqueKey(MapBuilder.of("name", processName)); + GetOutput getOutput = new GetAction().execute(getInput); + + //////////////////////// + // upon cache miss... // + //////////////////////// + if(getOutput.getRecord() == null) + { + QProcessMetaData processMetaData = qInstance.getProcess(processName); + if(processMetaData == null) + { + LOG.info("No such process", logPair("processName", processName)); + return (null); + } + + /////////////////////////////////////////////////////// + // insert the record (into the table, not the cache) // + /////////////////////////////////////////////////////// + InsertInput insertInput = new InsertInput(); + insertInput.setTableName(QQQProcess.TABLE_NAME); + insertInput.setRecords(List.of(new QRecord().withValue("name", processName).withValue("label", processMetaData.getLabel()))); + InsertOutput insertOutput = new InsertAction().execute(insertInput); + + /////////////////////////////////// + // repeat the get from the cache // + /////////////////////////////////// + getOutput = new GetAction().execute(getInput); + } + + return getOutput.getRecord().getValueInteger("id"); + } +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/processes/QQQProcessesMetaDataProvider.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/processes/QQQProcessesMetaDataProvider.java new file mode 100644 index 00000000..6cca0f6e --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/processes/QQQProcessesMetaDataProvider.java @@ -0,0 +1,134 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2025. 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.processes; + + +import java.util.function.Consumer; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.audits.AuditLevel; +import com.kingsrook.qqq.backend.core.model.metadata.audits.QAuditRules; +import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource; +import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType; +import com.kingsrook.qqq.backend.core.model.metadata.tables.Capability; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey; +import com.kingsrook.qqq.backend.core.model.metadata.tables.cache.CacheOf; +import com.kingsrook.qqq.backend.core.model.metadata.tables.cache.CacheUseCase; + + +/******************************************************************************* + ** Provides meta data for the QQQProcess table, PVS, and a cache table. + *******************************************************************************/ +public class QQQProcessesMetaDataProvider +{ + public static final String QQQ_PROCESS_CACHE_TABLE_NAME = QQQProcess.TABLE_NAME + "Cache"; + + + + /******************************************************************************* + ** + *******************************************************************************/ + public void defineAll(QInstance instance, String persistentBackendName, String cacheBackendName, Consumer backendDetailEnricher) throws QException + { + instance.addTable(defineQQQProcess(persistentBackendName, backendDetailEnricher)); + instance.addTable(defineQQQProcessCache(cacheBackendName, backendDetailEnricher)); + instance.addPossibleValueSource(defineQQQProcessPossibleValueSource()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public QTableMetaData defineQQQProcess(String backendName, Consumer backendDetailEnricher) throws QException + { + QTableMetaData table = new QTableMetaData() + .withName(QQQProcess.TABLE_NAME) + .withLabel("Process") + .withBackendName(backendName) + .withAuditRules(new QAuditRules().withAuditLevel(AuditLevel.NONE)) + .withRecordLabelFormat("%s") + .withRecordLabelFields("label") + .withPrimaryKeyField("id") + .withUniqueKey(new UniqueKey("name")) + .withFieldsFromEntity(QQQProcess.class) + .withoutCapabilities(Capability.TABLE_INSERT, Capability.TABLE_UPDATE, Capability.TABLE_DELETE); + + if(backendDetailEnricher != null) + { + backendDetailEnricher.accept(table); + } + + return (table); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public QTableMetaData defineQQQProcessCache(String backendName, Consumer backendDetailEnricher) throws QException + { + QTableMetaData table = new QTableMetaData() + .withName(QQQ_PROCESS_CACHE_TABLE_NAME) + .withBackendName(backendName) + .withAuditRules(new QAuditRules().withAuditLevel(AuditLevel.NONE)) + .withRecordLabelFormat("%s") + .withRecordLabelFields("label") + .withPrimaryKeyField("id") + .withUniqueKey(new UniqueKey("name")) + .withFieldsFromEntity(QQQProcess.class) + .withCacheOf(new CacheOf() + .withSourceTable(QQQProcess.TABLE_NAME) + .withUseCase(new CacheUseCase() + .withType(CacheUseCase.Type.UNIQUE_KEY_TO_UNIQUE_KEY) + .withCacheSourceMisses(false) + .withCacheUniqueKey(new UniqueKey("name")) + .withSourceUniqueKey(new UniqueKey("name")) + .withDoCopySourcePrimaryKeyToCache(true) + ) + ); + + if(backendDetailEnricher != null) + { + backendDetailEnricher.accept(table); + } + + return (table); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public QPossibleValueSource defineQQQProcessPossibleValueSource() + { + return (new QPossibleValueSource() + .withType(QPossibleValueSourceType.TABLE) + .withName(QQQProcess.TABLE_NAME) + .withTableName(QQQProcess.TABLE_NAME)) + .withOrderByField("label"); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/tables/QQQTable.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/tables/QQQTable.java index b0b607bf..2fe0b4fb 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/tables/QQQTable.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/tables/QQQTable.java @@ -30,7 +30,9 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior /******************************************************************************* - ** QRecord Entity for QQQTable table + ** QRecord Entity for QQQTable table - e.g., table that stores an id, name + ** and the label for all tables in the QQQ application. Useful as a foreign + ** key from other logging type tables. *******************************************************************************/ public class QQQTable extends QRecordEntity { diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/tables/QQQTableTableManager.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/tables/QQQTableTableManager.java new file mode 100644 index 00000000..d15aabb7 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/tables/QQQTableTableManager.java @@ -0,0 +1,92 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2025. 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.tables; + + +import java.util.List; +import com.kingsrook.qqq.backend.core.actions.tables.GetAction; +import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.logging.QLogger; +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.data.QRecord; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder; +import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; + + +/******************************************************************************* + ** Utility class for accessing QQQTable records (well, just their ids at this time) + ** Takes care of inserting upon a miss, and dealing with the cache table. + *******************************************************************************/ +public class QQQTableTableManager +{ + private static final QLogger LOG = QLogger.getLogger(QQQTableTableManager.class); + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static Integer getQQQTableId(QInstance qInstance, String tableName) throws QException + { + ///////////////////////////// + // look in the cache table // + ///////////////////////////// + GetInput getInput = new GetInput(); + getInput.setTableName(QQQTablesMetaDataProvider.QQQ_TABLE_CACHE_TABLE_NAME); + getInput.setUniqueKey(MapBuilder.of("name", tableName)); + GetOutput getOutput = new GetAction().execute(getInput); + + //////////////////////// + // upon cache miss... // + //////////////////////// + if(getOutput.getRecord() == null) + { + QTableMetaData tableMetaData = qInstance.getTable(tableName); + if(tableMetaData == null) + { + LOG.info("No such table", logPair("tableName", tableName)); + return (null); + } + + /////////////////////////////////////////////////////// + // insert the record (into the table, not the cache) // + /////////////////////////////////////////////////////// + InsertInput insertInput = new InsertInput(); + insertInput.setTableName(QQQTable.TABLE_NAME); + insertInput.setRecords(List.of(new QRecord().withValue("name", tableName).withValue("label", tableMetaData.getLabel()))); + InsertOutput insertOutput = new InsertAction().execute(insertInput); + + /////////////////////////////////// + // repeat the get from the cache // + /////////////////////////////////// + getOutput = new GetAction().execute(getInput); + } + + return getOutput.getRecord().getValueInteger("id"); + } +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/tables/QQQTablesMetaDataProvider.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/tables/QQQTablesMetaDataProvider.java index 034efcd5..e880a230 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/tables/QQQTablesMetaDataProvider.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/tables/QQQTablesMetaDataProvider.java @@ -37,7 +37,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.cache.CacheUseCase; /******************************************************************************* - ** + ** Provides meta data for the QQQTable table, PVS, and a cache table. *******************************************************************************/ public class QQQTablesMetaDataProvider { diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/processes/QQQProcessTableManagerTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/processes/QQQProcessTableManagerTest.java new file mode 100644 index 00000000..cf00fa81 --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/processes/QQQProcessTableManagerTest.java @@ -0,0 +1,95 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2025. 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.processes; + + +import com.kingsrook.qqq.backend.core.BaseTest; +import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; +import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; +import com.kingsrook.qqq.backend.core.context.QContext; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; +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.assertNull; + + +/******************************************************************************* + ** Unit test for QQQProcessTableManager + *******************************************************************************/ +class QQQProcessTableManagerTest extends BaseTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testProcessesGetInsertedUponRequest() throws QException + { + new QQQProcessesMetaDataProvider().defineAll(QContext.getQInstance(), TestUtils.MEMORY_BACKEND_NAME, TestUtils.MEMORY_BACKEND_NAME, null); + + Integer greetPeopleProcessId = QQQProcessTableManager.getQQQProcessId(QContext.getQInstance(), TestUtils.PROCESS_NAME_GREET_PEOPLE); + assertEquals(1, greetPeopleProcessId); + + assertEquals(1, QueryAction.execute(QQQProcessesMetaDataProvider.QQQ_PROCESS_CACHE_TABLE_NAME, new QQueryFilter()).size()); + assertEquals(1, QueryAction.execute(QQQProcess.TABLE_NAME, new QQueryFilter()).size()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testExistingProcessComesBack() throws QException + { + new QQQProcessesMetaDataProvider().defineAll(QContext.getQInstance(), TestUtils.MEMORY_BACKEND_NAME, TestUtils.MEMORY_BACKEND_NAME, null); + + new InsertAction().execute(new InsertInput(QQQProcess.TABLE_NAME).withRecordEntity(new QQQProcess().withName(TestUtils.PROCESS_NAME_GREET_PEOPLE))); + new InsertAction().execute(new InsertInput(QQQProcess.TABLE_NAME).withRecordEntity(new QQQProcess().withName(TestUtils.PROCESS_NAME_ADD_TO_PEOPLES_AGE))); + + assertEquals(0, QueryAction.execute(QQQProcessesMetaDataProvider.QQQ_PROCESS_CACHE_TABLE_NAME, new QQueryFilter()).size()); + assertEquals(2, QueryAction.execute(QQQProcess.TABLE_NAME, new QQueryFilter()).size()); + + assertEquals(2, QQQProcessTableManager.getQQQProcessId(QContext.getQInstance(), TestUtils.PROCESS_NAME_ADD_TO_PEOPLES_AGE)); + + assertEquals(1, QueryAction.execute(QQQProcessesMetaDataProvider.QQQ_PROCESS_CACHE_TABLE_NAME, new QQueryFilter()).size()); + assertEquals(2, QueryAction.execute(QQQProcess.TABLE_NAME, new QQueryFilter()).size()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testBogusProcessName() throws QException + { + new QQQProcessesMetaDataProvider().defineAll(QContext.getQInstance(), TestUtils.MEMORY_BACKEND_NAME, TestUtils.MEMORY_BACKEND_NAME, null); + assertNull(QQQProcessTableManager.getQQQProcessId(QContext.getQInstance(), "not a process")); + assertEquals(0, QueryAction.execute(QQQProcessesMetaDataProvider.QQQ_PROCESS_CACHE_TABLE_NAME, new QQueryFilter()).size()); + assertEquals(0, QueryAction.execute(QQQProcess.TABLE_NAME, new QQueryFilter()).size()); + } + +} \ No newline at end of file diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/tables/QQQTableTableManagerTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/tables/QQQTableTableManagerTest.java new file mode 100644 index 00000000..7d785363 --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/tables/QQQTableTableManagerTest.java @@ -0,0 +1,94 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2025. 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.tables; + + +import com.kingsrook.qqq.backend.core.BaseTest; +import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; +import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; +import com.kingsrook.qqq.backend.core.context.QContext; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; +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.assertNull; + + +/******************************************************************************* + ** Unit test for QQQTableTableManager + *******************************************************************************/ +class QQQTableTableManagerTest extends BaseTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testTablesGetInsertedUponRequest() throws QException + { + new QQQTablesMetaDataProvider().defineAll(QContext.getQInstance(), TestUtils.MEMORY_BACKEND_NAME, TestUtils.MEMORY_BACKEND_NAME, null); + + Integer personMemoryTableId = QQQTableTableManager.getQQQTableId(QContext.getQInstance(), TestUtils.TABLE_NAME_PERSON_MEMORY); + assertEquals(1, personMemoryTableId); + + assertEquals(1, QueryAction.execute(QQQTablesMetaDataProvider.QQQ_TABLE_CACHE_TABLE_NAME, new QQueryFilter()).size()); + assertEquals(1, QueryAction.execute(QQQTable.TABLE_NAME, new QQueryFilter()).size()); + } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testExistingTableComesBack() throws QException + { + new QQQTablesMetaDataProvider().defineAll(QContext.getQInstance(), TestUtils.MEMORY_BACKEND_NAME, TestUtils.MEMORY_BACKEND_NAME, null); + + new InsertAction().execute(new InsertInput(QQQTable.TABLE_NAME).withRecordEntity(new QQQTable().withName(TestUtils.TABLE_NAME_SHAPE))); + new InsertAction().execute(new InsertInput(QQQTable.TABLE_NAME).withRecordEntity(new QQQTable().withName(TestUtils.TABLE_NAME_ID_AND_NAME_ONLY))); + + assertEquals(0, QueryAction.execute(QQQTablesMetaDataProvider.QQQ_TABLE_CACHE_TABLE_NAME, new QQueryFilter()).size()); + assertEquals(2, QueryAction.execute(QQQTable.TABLE_NAME, new QQueryFilter()).size()); + + assertEquals(2, QQQTableTableManager.getQQQTableId(QContext.getQInstance(), TestUtils.TABLE_NAME_ID_AND_NAME_ONLY)); + + assertEquals(1, QueryAction.execute(QQQTablesMetaDataProvider.QQQ_TABLE_CACHE_TABLE_NAME, new QQueryFilter()).size()); + assertEquals(2, QueryAction.execute(QQQTable.TABLE_NAME, new QQueryFilter()).size()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testBogusTableName() throws QException + { + new QQQTablesMetaDataProvider().defineAll(QContext.getQInstance(), TestUtils.MEMORY_BACKEND_NAME, TestUtils.MEMORY_BACKEND_NAME, null); + assertNull(QQQTableTableManager.getQQQTableId(QContext.getQInstance(), "not a table")); + assertEquals(0, QueryAction.execute(QQQTablesMetaDataProvider.QQQ_TABLE_CACHE_TABLE_NAME, new QQueryFilter()).size()); + assertEquals(0, QueryAction.execute(QQQTable.TABLE_NAME, new QQueryFilter()).size()); + } + +} \ No newline at end of file From cd401775691c08692b7e162ae391ae45ea508900 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Mon, 10 Feb 2025 09:52:18 -0600 Subject: [PATCH 3/4] Add Long to isSupportedFieldType --- .../com/kingsrook/qqq/backend/core/model/data/QRecordEntity.java | 1 + 1 file changed, 1 insertion(+) diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecordEntity.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecordEntity.java index 6425893a..c07446a0 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecordEntity.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecordEntity.java @@ -530,6 +530,7 @@ public abstract class QRecordEntity // todo - more types!! return (returnType.equals(String.class) || returnType.equals(Integer.class) + || returnType.equals(Long.class) || returnType.equals(int.class) || returnType.equals(Boolean.class) || returnType.equals(boolean.class) From c47c39f5e7fb2528be2bacf2b2620624c800ed05 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Mon, 10 Feb 2025 13:55:22 -0600 Subject: [PATCH 4/4] Move call to traceStartOrResume to be after processUUID is initialized (for case when it isn't given) --- .../qqq/backend/core/actions/processes/RunProcessAction.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 b6236fef..a535ae47 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 @@ -118,8 +118,6 @@ public class RunProcessAction RunProcessOutput runProcessOutput = new RunProcessOutput(); - traceStartOrResume(runProcessInput, process); - ////////////////////////////////////////////////////////// // generate a UUID for the process, if one wasn't given // ////////////////////////////////////////////////////////// @@ -129,6 +127,8 @@ public class RunProcessAction } runProcessOutput.setProcessUUID(runProcessInput.getProcessUUID()); + traceStartOrResume(runProcessInput, process); + UUIDAndTypeStateKey stateKey = new UUIDAndTypeStateKey(UUID.fromString(runProcessInput.getProcessUUID()), StateType.PROCESS_STATUS); ProcessState processState = primeProcessState(runProcessInput, stateKey, process);