Cleanup from code review

This commit is contained in:
2022-11-01 16:12:32 -05:00
parent 165583cd98
commit 683b3c658d
46 changed files with 1662 additions and 165 deletions

View File

@ -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();

View File

@ -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<String, QFrontendTableMetaData> tables = new LinkedHashMap<>();
for(Map.Entry<String, QTableMetaData> 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);

View File

@ -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

View File

@ -23,6 +23,8 @@ 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;
@ -33,7 +35,17 @@ 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
{
@ -65,7 +77,20 @@ public class ExecuteCodeAction
Class<? extends QCodeExecutor> executorClass = (Class<? extends QCodeExecutor>) Class.forName(languageExecutor);
QCodeExecutor qCodeExecutor = executorClass.getConstructor().newInstance();
Serializable codeOutput = qCodeExecutor.execute(codeReference, input.getContext(), executionLogger);
////////////////////////////////////////////////////////////////////////////////////////////////////
// merge all of the input context, plus the input... input - into a context for the code executor //
////////////////////////////////////////////////////////////////////////////////////////////////////
Map<String, Serializable> 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);
}

View File

@ -30,7 +30,8 @@ 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
{
@ -38,6 +39,6 @@ public interface QCodeExecutor
/*******************************************************************************
**
*******************************************************************************/
Serializable execute(QCodeReference codeReference, Map<String, Serializable> context, QCodeExecutionLoggerInterface executionLogger) throws QCodeException;
Serializable execute(QCodeReference codeReference, Map<String, Serializable> inputContext, QCodeExecutionLoggerInterface executionLogger) throws QCodeException;
}

View File

@ -33,7 +33,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
/*******************************************************************************
**
** Java
*******************************************************************************/
public class QJavaExecutor implements QCodeExecutor
{
@ -42,9 +42,9 @@ public class QJavaExecutor implements QCodeExecutor
**
*******************************************************************************/
@Override
public Serializable execute(QCodeReference codeReference, Map<String, Serializable> input, QCodeExecutionLoggerInterface executionLogger) throws QCodeException
public Serializable execute(QCodeReference codeReference, Map<String, Serializable> inputContext, QCodeExecutionLoggerInterface executionLogger) throws QCodeException
{
Map<String, Object> context = new HashMap<>(input);
Map<String, Object> context = new HashMap<>(inputContext);
if(!context.containsKey("logger"))
{
context.put("logger", executionLogger);

View File

@ -24,9 +24,11 @@ package com.kingsrook.qqq.backend.core.actions.scripts;
import java.io.Serializable;
import java.util.HashMap;
import com.kingsrook.qqq.backend.core.actions.scripts.logging.HeaderAndDetailTableCodeExecutionLogger;
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;
@ -50,15 +52,34 @@ public class RunAssociatedScriptAction
*******************************************************************************/
public void run(RunAssociatedScriptInput input, RunAssociatedScriptOutput output) throws QException
{
Serializable scriptId = getScriptId(input);
Script script = getScript(input, scriptId);
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.setContext(new HashMap<>(input.getInputValues()));
executeCodeInput.setCodeReference(new QCodeReference().withInlineCode(scriptRevision.getContents()).withCodeType(QCodeType.JAVA_SCRIPT));
executeCodeInput.setExecutionLogger(new HeaderAndDetailTableCodeExecutionLogger(scriptRevision.getScriptId(), scriptRevision.getId()));
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);
@ -77,6 +98,12 @@ public class RunAssociatedScriptAction
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()));
}
@ -92,6 +119,13 @@ public class RunAssociatedScriptAction
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()));
}
@ -107,6 +141,11 @@ public class RunAssociatedScriptAction
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()));
}

View File

@ -25,6 +25,7 @@ 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;
@ -50,6 +51,13 @@ 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
@ -60,6 +68,8 @@ public class StoreAssociatedScriptAction
*******************************************************************************/
public void run(StoreAssociatedScriptInput input, StoreAssociatedScriptOutput output) throws QException
{
ActionHelper.validateSession(input);
QTableMetaData table = input.getTable();
Optional<AssociatedScript> optAssociatedScript = table.getAssociatedScripts().stream().filter(as -> as.getFieldName().equals(input.getFieldName())).findFirst();
if(optAssociatedScript.isEmpty())
@ -77,6 +87,7 @@ public class StoreAssociatedScriptAction
getInput.setSession(input.getSession());
getInput.setTableName(input.getTableName());
getInput.setPrimaryKey(input.getRecordPrimaryKey());
getInput.setShouldGenerateDisplayValues(true);
GetOutput getOutput = new GetAction().execute(getInput);
associatedRecord = getOutput.getRecord();
}
@ -100,8 +111,13 @@ public class StoreAssociatedScriptAction
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 //

View File

@ -22,14 +22,20 @@
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
{
@ -40,5 +46,20 @@ 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());
}
}

View File

@ -27,30 +27,26 @@ import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
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.model.data.QRecord;
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.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/*******************************************************************************
** Implementation of a code execution logger that logs into a header and child
** table
** 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 HeaderAndDetailTableCodeExecutionLogger implements QCodeExecutionLoggerInterface
public class BuildScriptLogAndScriptLogLineExecutionLogger implements QCodeExecutionLoggerInterface
{
private static final Logger LOG = LogManager.getLogger(HeaderAndDetailTableCodeExecutionLogger.class);
private static final Logger LOG = LogManager.getLogger(BuildScriptLogAndScriptLogLineExecutionLogger.class);
private QRecord header;
private List<QRecord> children = new ArrayList<>();
private ExecuteCodeInput executeCodeInput;
private QRecord scriptLog;
private List<QRecord> scriptLogLines = new ArrayList<>();
protected ExecuteCodeInput executeCodeInput;
private Serializable scriptId;
private Serializable scriptRevisionId;
@ -61,7 +57,17 @@ public class HeaderAndDetailTableCodeExecutionLogger implements QCodeExecutionLo
** Constructor
**
*******************************************************************************/
public HeaderAndDetailTableCodeExecutionLogger(Serializable scriptId, Serializable scriptRevisionId)
public BuildScriptLogAndScriptLogLineExecutionLogger()
{
}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public BuildScriptLogAndScriptLogLineExecutionLogger(Serializable scriptId, Serializable scriptRevisionId)
{
this.scriptId = scriptId;
this.scriptRevisionId = scriptRevisionId;
@ -78,7 +84,7 @@ public class HeaderAndDetailTableCodeExecutionLogger implements QCodeExecutionLo
.withValue("scriptId", scriptId)
.withValue("scriptRevisionId", scriptRevisionId)
.withValue("startTimestamp", Instant.now())
.withValue("input", truncate(executeCodeInput.getContext().toString())));
.withValue("input", truncate(executeCodeInput.getInput())));
}
@ -89,7 +95,7 @@ public class HeaderAndDetailTableCodeExecutionLogger implements QCodeExecutionLo
protected QRecord buildDetailLogRecord(String logLine)
{
return (new QRecord()
.withValue("scriptLogId", header.getValue("id"))
.withValue("scriptLogId", scriptLog.getValue("id"))
.withValue("timestamp", Instant.now())
.withValue("text", truncate(logLine)));
}
@ -99,9 +105,9 @@ public class HeaderAndDetailTableCodeExecutionLogger implements QCodeExecutionLo
/*******************************************************************************
**
*******************************************************************************/
private String truncate(String logLine)
private String truncate(Object o)
{
return StringUtils.safeTruncate(logLine, 1000, "...");
return StringUtils.safeTruncate(ValueUtils.getValueAsString(o), 1000, "...");
}
@ -109,22 +115,22 @@ public class HeaderAndDetailTableCodeExecutionLogger implements QCodeExecutionLo
/*******************************************************************************
**
*******************************************************************************/
private void updateHeaderAtEnd(Serializable output, Exception exception)
protected void updateHeaderAtEnd(Serializable output, Exception exception)
{
Instant startTimestamp = (Instant) header.getValue("startTimestamp");
Instant startTimestamp = (Instant) scriptLog.getValue("startTimestamp");
Instant endTimestamp = Instant.now();
header.setValue("endTimestamp", endTimestamp);
header.setValue("runTimeMillis", startTimestamp.until(endTimestamp, ChronoUnit.MILLIS));
scriptLog.setValue("endTimestamp", endTimestamp);
scriptLog.setValue("runTimeMillis", startTimestamp.until(endTimestamp, ChronoUnit.MILLIS));
if(exception != null)
{
header.setValue("hadError", true);
header.setValue("error", exception.getMessage());
scriptLog.setValue("hadError", true);
scriptLog.setValue("error", exception.getMessage());
}
else
{
header.setValue("hadError", false);
header.setValue("output", truncate(String.valueOf(output)));
scriptLog.setValue("hadError", false);
scriptLog.setValue("output", truncate(output));
}
}
@ -139,14 +145,7 @@ public class HeaderAndDetailTableCodeExecutionLogger implements QCodeExecutionLo
try
{
this.executeCodeInput = executeCodeInput;
QRecord scriptLog = buildHeaderRecord(executeCodeInput);
InsertInput insertInput = new InsertInput(executeCodeInput.getInstance());
insertInput.setSession(executeCodeInput.getSession());
insertInput.setTableName("scriptLog");
insertInput.setRecords(List.of(scriptLog));
InsertOutput insertOutput = new InsertAction().execute(insertInput);
this.header = insertOutput.getRecords().get(0);
this.scriptLog = buildHeaderRecord(executeCodeInput);
}
catch(Exception e)
{
@ -162,7 +161,7 @@ public class HeaderAndDetailTableCodeExecutionLogger implements QCodeExecutionLo
@Override
public void acceptLogLine(String logLine)
{
children.add(buildDetailLogRecord(logLine));
scriptLogLines.add(buildDetailLogRecord(logLine));
}
@ -173,7 +172,7 @@ public class HeaderAndDetailTableCodeExecutionLogger implements QCodeExecutionLo
@Override
public void acceptException(Exception exception)
{
store(null, exception);
updateHeaderAtEnd(null, exception);
}
@ -184,38 +183,39 @@ public class HeaderAndDetailTableCodeExecutionLogger implements QCodeExecutionLo
@Override
public void acceptExecutionEnd(Serializable output)
{
store(output, null);
updateHeaderAtEnd(output, null);
}
/*******************************************************************************
** Getter for scriptLog
**
*******************************************************************************/
private void store(Serializable output, Exception exception)
public QRecord getScriptLog()
{
try
{
updateHeaderAtEnd(output, exception);
UpdateInput updateInput = new UpdateInput(executeCodeInput.getInstance());
updateInput.setSession(executeCodeInput.getSession());
updateInput.setTableName("scriptLog");
updateInput.setRecords(List.of(header));
new UpdateAction().execute(updateInput);
if(CollectionUtils.nullSafeHasContents(children))
{
InsertInput insertInput = new InsertInput(executeCodeInput.getInstance());
insertInput.setSession(executeCodeInput.getSession());
insertInput.setTableName("scriptLogLine");
insertInput.setRecords(children);
new InsertAction().execute(insertInput);
}
}
catch(Exception e)
{
LOG.warn("Error storing script log", e);
}
return scriptLog;
}
/*******************************************************************************
** Getter for scriptLogLines
**
*******************************************************************************/
public List<QRecord> getScriptLogLines()
{
return scriptLogLines;
}
/*******************************************************************************
** Setter for scriptLog
**
*******************************************************************************/
protected void setScriptLog(QRecord scriptLog)
{
this.scriptLog = scriptLog;
}
}

View File

@ -33,7 +33,7 @@ import org.apache.logging.log4j.Logger;
/*******************************************************************************
** Implementation of a code execution logger that just logs to LOG 4j
** Implementation of a code execution logger that logs to LOG 4j
*******************************************************************************/
public class Log4jCodeExecutionLogger implements QCodeExecutionLoggerInterface
{
@ -52,8 +52,8 @@ public class Log4jCodeExecutionLogger implements QCodeExecutionLoggerInterface
{
this.qCodeReference = executeCodeInput.getCodeReference();
String contextString = StringUtils.safeTruncate(ValueUtils.getValueAsString(executeCodeInput.getContext()), 250, "...");
LOG.info("Starting script execution: " + qCodeReference.getName() + ", uuid: " + uuid + ", with context: " + contextString);
String inputString = StringUtils.safeTruncate(ValueUtils.getValueAsString(executeCodeInput.getInput()), 250, "...");
LOG.info("Starting script execution: " + qCodeReference.getName() + ", uuid: " + uuid + ", with input: " + inputString);
}

View File

@ -27,23 +27,24 @@ 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)
{
@ -51,12 +52,12 @@ public interface QCodeExecutionLoggerInterface
}
/*******************************************************************************
**
** Called if the script fails with an exception.
*******************************************************************************/
void acceptException(Exception exception);
/*******************************************************************************
**
** Called if the script completes without exception.
*******************************************************************************/
void acceptExecutionEnd(Serializable output);

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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);
}
}
}

View File

@ -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);
}
}

View File

@ -184,6 +184,7 @@ public class QValueFormatter
}
catch(Exception e)
{
LOG.debug("Error formatting record label", e);
return (formatRecordLabelExceptionalCases(table, record));
}
}

View File

@ -23,8 +23,10 @@ package com.kingsrook.qqq.backend.core.exceptions;
/*******************************************************************************
* Exception thrown while generating reports
*
** 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
{

View File

@ -37,6 +37,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
public class ExecuteCodeInput extends AbstractActionInput
{
private QCodeReference codeReference;
private Map<String, Serializable> input;
private Map<String, Serializable> context;
private QCodeExecutionLoggerInterface executionLogger;
@ -87,6 +88,56 @@ public class ExecuteCodeInput extends AbstractActionInput
/*******************************************************************************
** Getter for input
**
*******************************************************************************/
public Map<String, Serializable> getInput()
{
return input;
}
/*******************************************************************************
** Setter for input
**
*******************************************************************************/
public void setInput(Map<String, Serializable> input)
{
this.input = input;
}
/*******************************************************************************
** Fluent setter for input
**
*******************************************************************************/
public ExecuteCodeInput withInput(Map<String, Serializable> 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
**

View File

@ -22,6 +22,7 @@
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;
@ -34,7 +35,9 @@ import com.kingsrook.qqq.backend.core.model.metadata.code.AssociatedScriptCodeRe
public class RunAssociatedScriptInput extends AbstractTableActionInput
{
private AssociatedScriptCodeReference codeReference;
private Map<String, String> inputValues;
private Map<String, Serializable> inputValues;
private Serializable outputObject;
@ -86,7 +89,7 @@ public class RunAssociatedScriptInput extends AbstractTableActionInput
** Getter for inputValues
**
*******************************************************************************/
public Map<String, String> getInputValues()
public Map<String, Serializable> getInputValues()
{
return inputValues;
}
@ -97,7 +100,7 @@ public class RunAssociatedScriptInput extends AbstractTableActionInput
** Setter for inputValues
**
*******************************************************************************/
public void setInputValues(Map<String, String> inputValues)
public void setInputValues(Map<String, Serializable> inputValues)
{
this.inputValues = inputValues;
}
@ -108,10 +111,44 @@ public class RunAssociatedScriptInput extends AbstractTableActionInput
** Fluent setter for inputValues
**
*******************************************************************************/
public RunAssociatedScriptInput withInputValues(Map<String, String> inputValues)
public RunAssociatedScriptInput withInputValues(Map<String, Serializable> 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);
}
}

View File

@ -22,8 +22,12 @@
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.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 +42,9 @@ public class QBackendMetaData
private String name;
private String backendType;
private Set<Capability> enabledCapabilities = new HashSet<>();
private Set<Capability> disabledCapabilities = new HashSet<>();
// todo - at some point, we may want to apply this to secret properties on subclasses?
// @JsonFilter("secretsFilter")
@ -157,4 +164,161 @@ public class QBackendMetaData
// noop in base class //
////////////////////////
}
/*******************************************************************************
** Getter for enabledCapabilities
**
*******************************************************************************/
public Set<Capability> getEnabledCapabilities()
{
return enabledCapabilities;
}
/*******************************************************************************
** Setter for enabledCapabilities
**
*******************************************************************************/
public void setEnabledCapabilities(Set<Capability> enabledCapabilities)
{
this.enabledCapabilities = enabledCapabilities;
}
/*******************************************************************************
** Fluent setter for enabledCapabilities
**
*******************************************************************************/
public QBackendMetaData withEnabledCapabilities(Set<Capability> enabledCapabilities)
{
this.enabledCapabilities = enabledCapabilities;
return (this);
}
/*******************************************************************************
** Alternative fluent setter for enabledCapabilities
**
*******************************************************************************/
public QBackendMetaData withCapabilities(Set<Capability> 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<Capability> getDisabledCapabilities()
{
return disabledCapabilities;
}
/*******************************************************************************
** Setter for disabledCapabilities
**
*******************************************************************************/
public void setDisabledCapabilities(Set<Capability> disabledCapabilities)
{
this.disabledCapabilities = disabledCapabilities;
}
/*******************************************************************************
** Fluent setter for disabledCapabilities
**
*******************************************************************************/
public QBackendMetaData withDisabledCapabilities(Set<Capability> 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<Capability> 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);
}
}

View File

@ -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<String, Serializable> defaultValues = new LinkedHashMap<>();
/*******************************************************************************
@ -242,4 +247,56 @@ public class QWidgetMetaData implements QWidgetMetaDataInterface
return (this);
}
/*******************************************************************************
** Getter for defaultValues
**
*******************************************************************************/
public Map<String, Serializable> getDefaultValues()
{
return defaultValues;
}
/*******************************************************************************
** Setter for defaultValues
**
*******************************************************************************/
public void setDefaultValues(Map<String, Serializable> defaultValues)
{
this.defaultValues = defaultValues;
}
/*******************************************************************************
** Fluent setter for defaultValues
**
*******************************************************************************/
public QWidgetMetaData withDefaultValues(Map<String, Serializable> 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);
}
}

View File

@ -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 //
//////////////////////////////////////////////////////////////////////////

View File

@ -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<String> widgets;
private Set<String> 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<Capability> 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<String> getCapabilities()
{
return capabilities;
}
}

View File

@ -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);
}
}

View File

@ -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
**

View File

@ -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<QPossibleValue<?>> enumValues)
{
this.enumValues = enumValues;
setType(QPossibleValueSourceType.ENUM);
}
@ -456,7 +458,7 @@ public class QPossibleValueSource
*******************************************************************************/
public QPossibleValueSource withEnumValues(List<QPossibleValue<?>> 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);
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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 //
//////////////////////////////////////////////////////////////////////////
}

View File

@ -38,6 +38,8 @@ public class QFieldSection
private List<String> 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);
}
}

View File

@ -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;
@ -81,6 +83,9 @@ public class QTableMetaData implements QAppChildMetaData, Serializable
private List<String> widgets;
private List<AssociatedScript> associatedScripts;
private Set<Capability> enabledCapabilities = new HashSet<>();
private Set<Capability> disabledCapabilities = new HashSet<>();
/*******************************************************************************
@ -874,4 +879,160 @@ public class QTableMetaData implements QAppChildMetaData, Serializable
return (this);
}
/*******************************************************************************
** Getter for enabledCapabilities
**
*******************************************************************************/
public Set<Capability> getEnabledCapabilities()
{
return enabledCapabilities;
}
/*******************************************************************************
** Setter for enabledCapabilities
**
*******************************************************************************/
public void setEnabledCapabilities(Set<Capability> enabledCapabilities)
{
this.enabledCapabilities = enabledCapabilities;
}
/*******************************************************************************
** Fluent setter for enabledCapabilities
**
*******************************************************************************/
public QTableMetaData withEnabledCapabilities(Set<Capability> enabledCapabilities)
{
this.enabledCapabilities = enabledCapabilities;
return (this);
}
/*******************************************************************************
** Alternative fluent setter for enabledCapabilities
**
*******************************************************************************/
public QTableMetaData withCapabilities(Set<Capability> 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<Capability> getDisabledCapabilities()
{
return disabledCapabilities;
}
/*******************************************************************************
** Setter for disabledCapabilities
**
*******************************************************************************/
public void setDisabledCapabilities(Set<Capability> disabledCapabilities)
{
this.disabledCapabilities = disabledCapabilities;
}
/*******************************************************************************
** Fluent setter for disabledCapabilities
**
*******************************************************************************/
public QTableMetaData withDisabledCapabilities(Set<Capability> 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<Capability> 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);
}
}

View File

@ -48,10 +48,10 @@ public class Script extends QRecordEntity
@QField()
private String name;
@QField()
@QField(possibleValueSourceName = "scriptType")
private Integer scriptTypeId;
@QField()
@QField(possibleValueSourceName = "scriptRevision")
private Integer currentScriptRevisionId;

View File

@ -25,6 +25,7 @@ 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;
/*******************************************************************************
@ -43,10 +44,10 @@ public class ScriptLog extends QRecordEntity
@QField()
private Instant modifyDate;
@QField()
@QField(possibleValueSourceName = "script")
private Integer scriptId;
@QField()
@QField(possibleValueSourceName = "scriptRevision")
private Integer scriptRevisionId;
@QField()
@ -55,7 +56,7 @@ public class ScriptLog extends QRecordEntity
@QField()
private Instant endTimestamp;
@QField()
@QField(displayFormat = DisplayFormat.COMMAS)
private Integer runTimeMillis;
@QField()

View File

@ -43,7 +43,7 @@ public class ScriptLogLine extends QRecordEntity
@QField()
private Instant modifyDate;
@QField()
@QField(possibleValueSourceName = "scriptLog")
private Integer scriptLogId;
@QField()

View File

@ -45,7 +45,7 @@ public class ScriptRevision extends QRecordEntity
@QField()
private Instant modifyDate;
@QField()
@QField(possibleValueSourceName = "script")
private Integer scriptId;
@QField()

View File

@ -28,7 +28,14 @@ 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;
/*******************************************************************************
@ -37,6 +44,17 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
public class ScriptsMetaDataProvider
{
/*******************************************************************************
**
*******************************************************************************/
public void defineAll(QInstance instance, String backendName, Consumer<QTableMetaData> backendDetailEnricher) throws QException
{
defineStandardScriptsTables(instance, backendName, backendDetailEnricher);
defineStandardScriptsPossibleValueSources(instance);
}
/*******************************************************************************
**
*******************************************************************************/
@ -53,7 +71,35 @@ public class ScriptsMetaDataProvider
/*******************************************************************************
**
*******************************************************************************/
public List<QTableMetaData> defineStandardScriptsTables(String backendName, Consumer<QTableMetaData> backendDetailEnricher) throws QException
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<QTableMetaData> defineStandardScriptsTables(String backendName, Consumer<QTableMetaData> backendDetailEnricher) throws QException
{
List<QTableMetaData> rs = new ArrayList<>();
rs.add(enrich(backendDetailEnricher, defineScriptTypeTable(backendName)));
@ -101,7 +147,9 @@ public class ScriptsMetaDataProvider
*******************************************************************************/
private QTableMetaData defineScriptTable(String backendName) throws QException
{
return (defineStandardTable(backendName, Script.TABLE_NAME, Script.class));
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"))));
}
@ -111,7 +159,12 @@ public class ScriptsMetaDataProvider
*******************************************************************************/
private QTableMetaData defineScriptTypeTable(String backendName) throws QException
{
return (defineStandardTable(backendName, ScriptType.TABLE_NAME, ScriptType.class));
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);
}
@ -121,8 +174,17 @@ public class ScriptsMetaDataProvider
*******************************************************************************/
private QTableMetaData defineScriptRevisionTable(String backendName) throws QException
{
return (defineStandardTable(backendName, ScriptRevision.TABLE_NAME, ScriptRevision.class)
.withRecordLabelFields(List.of("id")));
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);
}
@ -133,7 +195,12 @@ public class ScriptsMetaDataProvider
private QTableMetaData defineScriptLogTable(String backendName) throws QException
{
return (defineStandardTable(backendName, ScriptLog.TABLE_NAME, ScriptLog.class)
.withRecordLabelFields(List.of("id")));
.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"))));
}

View File

@ -28,7 +28,11 @@ 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
{

View File

@ -39,12 +39,16 @@ 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)
@ -407,7 +411,7 @@ public class BackendQueryFilterUtils
/*******************************************************************************
**
** Sort list of records based on filter.
*******************************************************************************/
public static void sortRecordList(QQueryFilter filter, List<QRecord> recordList)
{
@ -443,7 +447,7 @@ public class BackendQueryFilterUtils
/*******************************************************************************
**
** Apply skip & limit attributes from queryInput to a list of records.
*******************************************************************************/
public static List<QRecord> applySkipAndLimit(QueryInput queryInput, List<QRecord> recordList)
{

View File

@ -41,13 +41,19 @@ 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<Serializable, QRecord> getForeignRecordMap(AbstractActionInput parentActionInput, List<QRecord> sourceRecords, String sourceTableForeignKeyFieldName, String foreignTableName, String foreignTablePrimaryKeyName) throws QException
{
@ -69,9 +75,14 @@ public class GeneralProcessUtils
/*******************************************************************************
** 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<Serializable, QRecord> getForeignRecordListingHashMap(AbstractActionInput parentActionInput, List<QRecord> sourceRecords, String sourceTableForeignKeyFieldName, String foreignTableName, String foreignTablePrimaryKeyName) throws QException
public static ListingHash<Serializable, QRecord> getForeignRecordListingHashMap(AbstractActionInput parentActionInput, List<QRecord> sourceRecords, String sourceTableForeignKeyFieldName, String foreignTableName, String foreignTableForeignKeyName) throws QException
{
ListingHash<Serializable, QRecord> foreignRecordMap = new ListingHash<>();
QueryInput queryInput = new QueryInput(parentActionInput.getInstance());
@ -79,11 +90,11 @@ public class GeneralProcessUtils
queryInput.setTableName(foreignTableName);
List<Serializable> foreignIds = new ArrayList<>(sourceRecords.stream().map(r -> r.getValue(sourceTableForeignKeyFieldName)).toList());
queryInput.setFilter(new QQueryFilter().withCriteria(new QFilterCriteria(foreignTablePrimaryKeyName, QCriteriaOperator.IN, foreignIds)));
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(foreignTablePrimaryKeyName), foreignRecord);
foreignRecordMap.add(foreignRecord.getValue(foreignTableForeignKeyName), foreignRecord);
}
return foreignRecordMap;
}
@ -91,7 +102,13 @@ public class GeneralProcessUtils
/*******************************************************************************
** 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<QRecord> sourceRecords, String sourceTableForeignKeyFieldName, String foreignTableName, String foreignTablePrimaryKeyName) throws QException
{
@ -106,11 +123,16 @@ public class GeneralProcessUtils
/*******************************************************************************
** 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<QRecord> sourceRecords, String sourceTableForeignKeyFieldName, String foreignTableName, String foreignTablePrimaryKeyName) throws QException
public static void addForeignRecordsListToRecordList(AbstractActionInput parentActionInput, List<QRecord> sourceRecords, String sourceTableForeignKeyFieldName, String foreignTableName, String foreignTableForeignKeyName) throws QException
{
ListingHash<Serializable, QRecord> foreignRecordMap = getForeignRecordListingHashMap(parentActionInput, sourceRecords, sourceTableForeignKeyFieldName, foreignTableName, foreignTablePrimaryKeyName);
ListingHash<Serializable, QRecord> foreignRecordMap = getForeignRecordListingHashMap(parentActionInput, sourceRecords, sourceTableForeignKeyFieldName, foreignTableName, foreignTableForeignKeyName);
for(QRecord sourceRecord : sourceRecords)
{
List<QRecord> foreignRecordList = foreignRecordMap.get(sourceRecord.getValue(sourceTableForeignKeyFieldName));
@ -131,7 +153,8 @@ public class GeneralProcessUtils
/*******************************************************************************
**
** Run a query on tableName, for where fieldName equals fieldValue, and return
** the list of QRecords.
*******************************************************************************/
public static List<QRecord> getRecordListByField(AbstractActionInput parentActionInput, String tableName, String fieldName, Serializable fieldValue) throws QException
{
@ -146,7 +169,9 @@ public class GeneralProcessUtils
/*******************************************************************************
**
** 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<QRecord> getRecordById(AbstractActionInput parentActionInput, String tableName, String fieldName, Serializable fieldValue) throws QException
{
@ -162,7 +187,10 @@ public class GeneralProcessUtils
/*******************************************************************************
** 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<QRecord> loadTable(AbstractActionInput parentActionInput, String tableName) throws QException
{
@ -176,7 +204,15 @@ public class GeneralProcessUtils
/*******************************************************************************
** 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<Serializable, QRecord> loadTableToMap(AbstractActionInput parentActionInput, String tableName, String keyFieldName) throws QException
{
@ -201,7 +237,14 @@ public class GeneralProcessUtils
/*******************************************************************************
** 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<Serializable, QRecord> loadTableToListingHash(AbstractActionInput parentActionInput, String tableName, String keyFieldName) throws QException
{

View File

@ -26,10 +26,10 @@ 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.HeaderAndDetailTableCodeExecutionLogger;
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;
@ -120,9 +120,9 @@ class ExecuteCodeActionTest
void testTableLogger() throws QException
{
QInstance qInstance = TestUtils.defineInstance();
new ScriptsMetaDataProvider().defineStandardScriptsTables(qInstance, TestUtils.MEMORY_BACKEND_NAME, null);
new ScriptsMetaDataProvider().defineAll(qInstance, TestUtils.MEMORY_BACKEND_NAME, null);
ExecuteCodeInput executeCodeInput = setupInput(qInstance, Map.of("x", 4), new HeaderAndDetailTableCodeExecutionLogger(1701, 1702));
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());

View File

@ -25,11 +25,17 @@ 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;
@ -56,24 +62,7 @@ class RunAssociatedScriptActionTest
@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")
);
new ScriptsMetaDataProvider().defineStandardScriptsTables(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")
));
QInstance instance = setupInstance();
insertScript(instance, 1, """
return "Hello";
@ -90,6 +79,9 @@ class RunAssociatedScriptActionTest
);
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)
@ -98,6 +90,204 @@ class RunAssociatedScriptActionTest
/*******************************************************************************
**
*******************************************************************************/
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.*");
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -24,15 +24,12 @@ package com.kingsrook.qqq.backend.core.actions.scripts;
import java.io.Serializable;
import java.util.List;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
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.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.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;
@ -47,6 +44,7 @@ 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;
/*******************************************************************************
@ -85,7 +83,7 @@ class StoreAssociatedScriptActionTest
.withFieldName("otherScriptId")
);
new ScriptsMetaDataProvider().defineStandardScriptsTables(instance, TestUtils.MEMORY_BACKEND_NAME, null);
new ScriptsMetaDataProvider().defineAll(instance, TestUtils.MEMORY_BACKEND_NAME, null);
TestUtils.insertRecords(instance, table, List.of(
new QRecord().withValue("id", 1),
@ -149,7 +147,6 @@ class StoreAssociatedScriptActionTest
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);
}
@ -157,16 +154,19 @@ class StoreAssociatedScriptActionTest
/*******************************************************************************
**
*******************************************************************************/
private Serializable assertValueInField(QInstance instance, String tableName, Serializable recordId, String fieldName, Serializable value) throws QException
private void assertValueInField(QInstance instance, String tableName, Serializable recordId, String fieldName, Serializable value) throws QException
{
QueryInput queryInput = new QueryInput(instance);
queryInput.setSession(new QSession());
queryInput.setTableName(tableName);
queryInput.setFilter(new QQueryFilter().withCriteria(new QFilterCriteria("id", QCriteriaOperator.EQUALS, List.of(recordId))));
QueryOutput queryOutput = new QueryAction().execute(queryInput);
Serializable actual = queryOutput.getRecords().get(0).getValue(fieldName);
assertEquals(value, actual);
return (actual);
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 + "]");
}
}

View File

@ -27,6 +27,7 @@ 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;
@ -39,6 +40,7 @@ class TestScriptActionTest
**
*******************************************************************************/
@Test
@Disabled("Not yet done.")
void test() throws QException
{
QInstance instance = TestUtils.defineInstance();

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<QRecord> 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"));
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<QRecord> 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<QRecord> 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"));
}
}

View File

@ -51,6 +51,7 @@ 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.tables.QFieldSection;
@ -805,6 +806,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",
@ -831,6 +833,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",
@ -860,6 +863,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",