Merge branch 'feature/CTLE-422-api-for-scripts' into integration/sprint-25

# Conflicts:
#	qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/QueryAction.java
This commit is contained in:
2023-05-01 20:13:22 -05:00
58 changed files with 4200 additions and 1569 deletions

View File

@ -51,10 +51,18 @@ commands:
module: qqq-backend-module-filesystem
- store_jacoco_site:
module: qqq-backend-module-rdbms
- store_jacoco_site:
module: qqq-backend-module-api
- store_jacoco_site:
module: qqq-middleware-api
- store_jacoco_site:
module: qqq-middleware-javalin
- store_jacoco_site:
module: qqq-middleware-picocli
- store_jacoco_site:
module: qqq-middleware-slack
- store_jacoco_site:
module: qqq-language-support-javascript
- store_jacoco_site:
module: qqq-sample-project
- run:

View File

@ -278,6 +278,7 @@ public class PollingAutomationPerTableRunner implements Runnable
.withPriority(record.getValueInteger("priority"))
.withCodeReference(new QCodeReference(RunRecordScriptAutomationHandler.class))
.withValues(MapBuilder.of("scriptId", record.getValue("scriptId")))
.withIncludeRecordAssociations(true)
);
}
}
@ -392,6 +393,8 @@ public class PollingAutomationPerTableRunner implements Runnable
queryInput.setFilter(filter);
queryInput.setIncludeAssociations(action.getIncludeRecordAssociations());
return (new QueryAction().execute(queryInput).getRecords());
}

View File

@ -50,4 +50,13 @@ public interface DeleteInterface
return (false);
}
/*******************************************************************************
** Specify whether this particular module's delete action can & should fetch
** records before deleting them, e.g., for audits or "not-found-checks"
*******************************************************************************/
default boolean supportsPreFetchQuery()
{
return (true);
}
}

View File

@ -37,4 +37,14 @@ public interface UpdateInterface
**
*******************************************************************************/
UpdateOutput execute(UpdateInput updateInput) throws QException;
/*******************************************************************************
** Specify whether this particular module's update action can & should fetch
** records before updating them, e.g., for audits or "not-found-checks"
*******************************************************************************/
default boolean supportsPreFetchQuery()
{
return (true);
}
}

View File

@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.core.actions.reporting;
import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
@ -63,7 +64,7 @@ public class BufferedRecordPipe extends RecordPipe
**
*******************************************************************************/
@Override
public void addRecord(QRecord record)
public void addRecord(QRecord record) throws QException
{
buffer.add(record);
if(buffer.size() >= bufferSize)
@ -78,7 +79,7 @@ public class BufferedRecordPipe extends RecordPipe
/*******************************************************************************
**
*******************************************************************************/
public void finalFlush()
public void finalFlush() throws QException
{
if(!buffer.isEmpty())
{

View File

@ -26,10 +26,11 @@ import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.utils.SleepUtils;
import com.kingsrook.qqq.backend.core.utils.lambdas.UnsafeConsumer;
/*******************************************************************************
@ -47,7 +48,7 @@ public class RecordPipe
private boolean isTerminated = false;
private Consumer<List<QRecord>> postRecordActions = null;
private UnsafeConsumer<List<QRecord>, QException> postRecordActions = null;
/////////////////////////////////////
// See usage below for explanation //
@ -93,7 +94,7 @@ public class RecordPipe
/*******************************************************************************
** Add a record to the pipe. Will block if the pipe is full. Will noop if pipe is terminated.
*******************************************************************************/
public void addRecord(QRecord record)
public void addRecord(QRecord record) throws QException
{
if(isTerminated)
{
@ -109,7 +110,7 @@ public class RecordPipe
// (which we'll create as a field in this class, to avoid always re-constructing) //
////////////////////////////////////////////////////////////////////////////////////
singleRecordListForPostRecordActions.add(record);
postRecordActions.accept(singleRecordListForPostRecordActions);
postRecordActions.run(singleRecordListForPostRecordActions);
record = singleRecordListForPostRecordActions.remove(0);
}
@ -152,11 +153,11 @@ public class RecordPipe
/*******************************************************************************
** Add a list of records to the pipe. Will block if the pipe is full. Will noop if pipe is terminated.
*******************************************************************************/
public void addRecords(List<QRecord> records)
public void addRecords(List<QRecord> records) throws QException
{
if(postRecordActions != null)
{
postRecordActions.accept(records);
postRecordActions.run(records);
}
//////////////////////////////////////////////////////////////////////////////////////////////////
@ -207,7 +208,7 @@ public class RecordPipe
/*******************************************************************************
**
*******************************************************************************/
public void setPostRecordActions(Consumer<List<QRecord>> postRecordActions)
public void setPostRecordActions(UnsafeConsumer<List<QRecord>, QException> postRecordActions)
{
this.postRecordActions = postRecordActions;
}

View File

@ -0,0 +1,79 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.reporting;
import java.util.List;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
/*******************************************************************************
** Subclass of BufferedRecordPipe, which ultimately sends records down to an
** original RecordPipe.
**
** Meant to be used where: someone passed in a RecordPipe (so they have a reference
** to it, and they are waiting to read from it), but the producer knows that
** it will be better to buffer the records, so they want to use a buffered pipe
** (but they still need the records to end up in the original pipe - thus -
** it gets wrapped by an object of this class).
*******************************************************************************/
public class RecordPipeBufferedWrapper extends BufferedRecordPipe
{
private RecordPipe wrappedPipe;
/*******************************************************************************
** Constructor - uses default buffer size
**
*******************************************************************************/
public RecordPipeBufferedWrapper(RecordPipe wrappedPipe)
{
this.wrappedPipe = wrappedPipe;
}
/*******************************************************************************
** Constructor - customize buffer size.
**
*******************************************************************************/
public RecordPipeBufferedWrapper(Integer bufferSize, RecordPipe wrappedPipe)
{
super(bufferSize);
this.wrappedPipe = wrappedPipe;
}
/*******************************************************************************
** when it's time to actually add records into the pipe, actually add them
** into the wrapped pipe!
*******************************************************************************/
@Override
public void addRecords(List<QRecord> records) throws QException
{
wrappedPipe.addRecords(records);
}
}

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.actions.scripts;
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.scripts.ScriptRevision;
/*******************************************************************************
**
*******************************************************************************/
public interface AssociatedScriptContextPrimerInterface
{
/*******************************************************************************
**
*******************************************************************************/
void primeContext(ExecuteCodeInput executeCodeInput, ScriptRevision scriptRevision) throws QException;
}

View File

@ -25,13 +25,21 @@ package com.kingsrook.qqq.backend.core.actions.scripts;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
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.actions.scripts.logging.ScriptExecutionLoggerInterface;
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.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.scripts.AbstractRunScriptInput;
import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeInput;
import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeOutput;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType;
import com.kingsrook.qqq.backend.core.model.scripts.ScriptRevision;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
/*******************************************************************************
@ -49,6 +57,9 @@ import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
*******************************************************************************/
public class ExecuteCodeAction
{
private static final QLogger LOG = QLogger.getLogger(ExecuteCodeAction.class);
/*******************************************************************************
**
@ -68,10 +79,10 @@ public class ExecuteCodeAction
try
{
String languageExecutor = switch(codeReference.getCodeType())
{
case JAVA -> "com.kingsrook.qqq.backend.core.actions.scripts.QJavaExecutor";
case JAVA_SCRIPT -> "com.kingsrook.qqq.languages.javascript.QJavaScriptExecutor";
};
{
case JAVA -> "com.kingsrook.qqq.backend.core.actions.scripts.QJavaExecutor";
case JAVA_SCRIPT -> "com.kingsrook.qqq.languages.javascript.QJavaScriptExecutor";
};
@SuppressWarnings("unchecked")
Class<? extends QCodeExecutor> executorClass = (Class<? extends QCodeExecutor>) Class.forName(languageExecutor);
@ -108,6 +119,91 @@ public class ExecuteCodeAction
/*******************************************************************************
**
*******************************************************************************/
public static ExecuteCodeInput setupExecuteCodeInput(AbstractRunScriptInput<?> input, ScriptRevision scriptRevision)
{
ExecuteCodeInput executeCodeInput = new ExecuteCodeInput();
executeCodeInput.setInput(new HashMap<>(Objects.requireNonNullElseGet(input.getInputValues(), HashMap::new)));
executeCodeInput.setContext(new HashMap<>());
Map<String, Serializable> context = executeCodeInput.getContext();
if(input.getOutputObject() != null)
{
context.put("output", input.getOutputObject());
}
if(input.getScriptUtils() != null)
{
context.put("scriptUtils", input.getScriptUtils());
}
executeCodeInput.setCodeReference(new QCodeReference().withInlineCode(scriptRevision.getContents()).withCodeType(QCodeType.JAVA_SCRIPT)); // todo - code type as attribute of script!!
ExecuteCodeAction.addApiUtilityToContext(context, scriptRevision);
ExecuteCodeAction.setExecutionLoggerInExecuteCodeInput(input, scriptRevision, executeCodeInput);
return (executeCodeInput);
}
/*******************************************************************************
** Try to (dynamically) load the ApiScriptUtils object from the api middleware
** module -- in case the runtime doesn't have that module deployed (e.g, not in
** the project pom).
*******************************************************************************/
public static void addApiUtilityToContext(Map<String, Serializable> context, ScriptRevision scriptRevision)
{
if(!StringUtils.hasContent(scriptRevision.getApiName()) || !StringUtils.hasContent(scriptRevision.getApiVersion()))
{
return;
}
try
{
Class<?> apiScriptUtilsClass = Class.forName("com.kingsrook.qqq.api.utils.ApiScriptUtils");
Object apiScriptUtilsObject = apiScriptUtilsClass.getConstructor(String.class, String.class).newInstance(scriptRevision.getApiName(), scriptRevision.getApiVersion());
context.put("api", (Serializable) apiScriptUtilsObject);
}
catch(ClassNotFoundException e)
{
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// this is the only exception we're kinda expecting here - so catch for it specifically, and just log.trace - others, warn //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
LOG.trace("Couldn't load ApiScriptUtils class - qqq-middleware-api not on the classpath?");
}
catch(Exception e)
{
LOG.warn("Error adding api utility to script context", e);
}
}
/*******************************************************************************
**
*******************************************************************************/
private static void setExecutionLoggerInExecuteCodeInput(AbstractRunScriptInput<?> input, ScriptRevision scriptRevision, ExecuteCodeInput executeCodeInput)
{
/////////////////////////////////////////////////////////////////////////////////////////////////
// let caller supply a logger, or by default use StoreScriptLogAndScriptLogLineExecutionLogger //
/////////////////////////////////////////////////////////////////////////////////////////////////
QCodeExecutionLoggerInterface executionLogger = Objects.requireNonNullElseGet(input.getLogger(), () -> new StoreScriptLogAndScriptLogLineExecutionLogger(scriptRevision.getScriptId(), scriptRevision.getId()));
executeCodeInput.setExecutionLogger(executionLogger);
if(executionLogger instanceof ScriptExecutionLoggerInterface scriptExecutionLoggerInterface)
{
////////////////////////////////////////////////////////////////////////////////////////////////////
// if logger is aware of scripts (as opposed to a generic CodeExecution logger), give it the ids. //
////////////////////////////////////////////////////////////////////////////////////////////////////
scriptExecutionLoggerInterface.setScriptId(scriptRevision.getScriptId());
scriptExecutionLoggerInterface.setScriptRevisionId(scriptRevision.getId());
}
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -22,15 +22,14 @@
package com.kingsrook.qqq.backend.core.actions.scripts;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
import com.kingsrook.qqq.backend.core.actions.scripts.logging.QCodeExecutionLoggerInterface;
import com.kingsrook.qqq.backend.core.actions.scripts.logging.ScriptExecutionLoggerInterface;
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.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.context.QContext;
@ -49,8 +48,6 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
import com.kingsrook.qqq.backend.core.model.metadata.code.AdHocScriptCodeReference;
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;
import com.kingsrook.qqq.backend.core.model.scripts.Script;
import com.kingsrook.qqq.backend.core.model.scripts.ScriptRevision;
@ -96,6 +93,7 @@ public class RunAdHocRecordScriptAction
QueryInput queryInput = new QueryInput();
queryInput.setTableName(input.getTableName());
queryInput.setFilter(new QQueryFilter(new QFilterCriteria(table.getPrimaryKeyField(), QCriteriaOperator.IN, input.getRecordPrimaryKeyList())));
queryInput.setIncludeAssociations(true);
QueryOutput queryOutput = new QueryAction().execute(queryInput);
input.setRecordList(queryOutput.getRecords());
}
@ -112,43 +110,14 @@ public class RunAdHocRecordScriptAction
/////////////
// run it! //
/////////////
ExecuteCodeInput executeCodeInput = new ExecuteCodeInput();
executeCodeInput.setInput(new HashMap<>(Objects.requireNonNullElseGet(input.getInputValues(), HashMap::new)));
executeCodeInput.getInput().put("records", new ArrayList<>(input.getRecordList()));
executeCodeInput.setContext(new HashMap<>());
if(input.getOutputObject() != null)
{
executeCodeInput.getContext().put("output", input.getOutputObject());
}
if(input.getScriptUtils() != null)
{
executeCodeInput.getContext().put("scriptUtils", input.getScriptUtils());
}
executeCodeInput.getContext().put("api", new ScriptApi());
executeCodeInput.setCodeReference(new QCodeReference().withInlineCode(scriptRevision.getContents()).withCodeType(QCodeType.JAVA_SCRIPT)); // todo - code type as attribute of script!!
/////////////////////////////////////////////////////////////////////////////////////////////////
// let caller supply a logger, or by default use StoreScriptLogAndScriptLogLineExecutionLogger //
/////////////////////////////////////////////////////////////////////////////////////////////////
QCodeExecutionLoggerInterface executionLogger = Objects.requireNonNullElseGet(input.getLogger(), () -> new StoreScriptLogAndScriptLogLineExecutionLogger(scriptRevision.getScriptId(), scriptRevision.getId()));
executeCodeInput.setExecutionLogger(executionLogger);
if(executionLogger instanceof ScriptExecutionLoggerInterface scriptExecutionLoggerInterface)
{
////////////////////////////////////////////////////////////////////////////////////////////////////
// if logger is aware of scripts (as opposed to a generic CodeExecution logger), give it the ids. //
////////////////////////////////////////////////////////////////////////////////////////////////////
scriptExecutionLoggerInterface.setScriptId(scriptRevision.getScriptId());
scriptExecutionLoggerInterface.setScriptRevisionId(scriptRevision.getId());
}
ExecuteCodeInput executeCodeInput = ExecuteCodeAction.setupExecuteCodeInput(input, scriptRevision);
executeCodeInput.getInput().put("records", getRecordsForScript(input, scriptRevision));
ExecuteCodeOutput executeCodeOutput = new ExecuteCodeOutput();
new ExecuteCodeAction().run(executeCodeInput, executeCodeOutput);
output.setOutput(executeCodeOutput.getOutput());
output.setLogger(executionLogger);
output.setLogger(executeCodeInput.getExecutionLogger());
}
catch(Exception e)
{
@ -158,6 +127,37 @@ public class RunAdHocRecordScriptAction
/*******************************************************************************
**
*******************************************************************************/
private static ArrayList<? extends Serializable> getRecordsForScript(RunAdHocRecordScriptInput input, ScriptRevision scriptRevision)
{
try
{
Class<?> apiScriptUtilsClass = Class.forName("com.kingsrook.qqq.api.utils.ApiScriptUtils");
Method qRecordListToApiRecordList = apiScriptUtilsClass.getMethod("qRecordListToApiRecordList", List.class, String.class, String.class, String.class);
Object apiRecordList = qRecordListToApiRecordList.invoke(null, input.getRecordList(), input.getTableName(), scriptRevision.getApiName(), scriptRevision.getApiVersion());
// noinspection unchecked
return (ArrayList<? extends Serializable>) apiRecordList;
}
catch(ClassNotFoundException e)
{
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// this is the only exception we're kinda expecting here - so catch for it specifically, and just log.trace - others, warn //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
LOG.trace("Couldn't load ApiScriptUtils class - qqq-middleware-api not on the classpath?");
}
catch(Exception e)
{
LOG.warn("Error converting QRecord list to api record list", e);
}
return (new ArrayList<>(input.getRecordList()));
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -25,11 +25,7 @@ package com.kingsrook.qqq.backend.core.actions.scripts;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
import com.kingsrook.qqq.backend.core.actions.scripts.logging.QCodeExecutionLoggerInterface;
import com.kingsrook.qqq.backend.core.actions.scripts.logging.ScriptExecutionLoggerInterface;
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;
@ -40,8 +36,6 @@ import com.kingsrook.qqq.backend.core.model.actions.scripts.RunAssociatedScriptO
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
import com.kingsrook.qqq.backend.core.model.metadata.code.AssociatedScriptCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType;
import com.kingsrook.qqq.backend.core.model.scripts.Script;
import com.kingsrook.qqq.backend.core.model.scripts.ScriptRevision;
@ -54,6 +48,7 @@ public class RunAssociatedScriptAction
private Map<AssociatedScriptCodeReference, ScriptRevision> scriptRevisionCache = new HashMap<>();
/*******************************************************************************
**
*******************************************************************************/
@ -61,35 +56,12 @@ public class RunAssociatedScriptAction
{
ActionHelper.validateSession(input);
ScriptRevision scriptRevision = getScriptRevision(input);
ScriptRevision scriptRevision = getScriptRevision(input);
ExecuteCodeInput executeCodeInput = ExecuteCodeAction.setupExecuteCodeInput(input, scriptRevision);
ExecuteCodeInput executeCodeInput = new ExecuteCodeInput();
executeCodeInput.setInput(new HashMap<>(input.getInputValues()));
executeCodeInput.setContext(new HashMap<>());
if(input.getOutputObject() != null)
if(input.getAssociatedScriptContextPrimerInterface() != null)
{
executeCodeInput.getContext().put("output", input.getOutputObject());
}
if(input.getScriptUtils() != null)
{
executeCodeInput.getContext().put("scriptUtils", input.getScriptUtils());
}
executeCodeInput.setCodeReference(new QCodeReference().withInlineCode(scriptRevision.getContents()).withCodeType(QCodeType.JAVA_SCRIPT)); // todo - code type as attribute of script!!
/////////////////////////////////////////////////////////////////////////////////////////////////
// let caller supply a logger, or by default use StoreScriptLogAndScriptLogLineExecutionLogger //
/////////////////////////////////////////////////////////////////////////////////////////////////
QCodeExecutionLoggerInterface executionLogger = Objects.requireNonNullElseGet(input.getLogger(), () -> new StoreScriptLogAndScriptLogLineExecutionLogger(scriptRevision.getScriptId(), scriptRevision.getId()));
executeCodeInput.setExecutionLogger(executionLogger);
if(executionLogger instanceof ScriptExecutionLoggerInterface scriptExecutionLoggerInterface)
{
////////////////////////////////////////////////////////////////////////////////////////////////////
// if logger is aware of scripts (as opposed to a generic CodeExecution logger), give it the ids. //
////////////////////////////////////////////////////////////////////////////////////////////////////
scriptExecutionLoggerInterface.setScriptId(scriptRevision.getScriptId());
scriptExecutionLoggerInterface.setScriptRevisionId(scriptRevision.getId());
input.getAssociatedScriptContextPrimerInterface().primeContext(executeCodeInput, scriptRevision);
}
ExecuteCodeOutput executeCodeOutput = new ExecuteCodeOutput();

View File

@ -47,6 +47,11 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
/*******************************************************************************
** Object made available to scripts for access to qqq api (e.g., query, insert,
** etc, plus object constructors).
**
** Before scripts knew about the API, this class made sense and was used.
** But, now that scripts do know about the API, it feels like this class could
** be deleted... but, what about, a QQQ deployment without the API module...
** In that case, we might still want this class... think about it.
*******************************************************************************/
public class ScriptApi implements Serializable
{

View File

@ -184,6 +184,8 @@ public class StoreAssociatedScriptAction
QRecord scriptRevision = new QRecord()
.withValue("scriptId", script.getValue("id"))
.withValue("contents", input.getCode())
.withValue("apiName", input.getApiName())
.withValue("apiVersion", input.getApiVersion())
.withValue("commitMessage", commitMessage)
.withValue("sequenceNo", nextSequenceNo);

View File

@ -32,6 +32,7 @@ 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.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.scripts.ScriptRevision;
/*******************************************************************************
@ -47,7 +48,7 @@ public interface TestScriptActionInterface
** Note - such a method may want or need to put an "output" object into the
** executeCodeInput's context map.
*******************************************************************************/
void setupTestScriptInput(TestScriptInput testScriptInput, ExecuteCodeInput executeCodeInput);
void setupTestScriptInput(TestScriptInput testScriptInput, ExecuteCodeInput executeCodeInput) throws QException;
/*******************************************************************************
@ -87,12 +88,21 @@ public interface TestScriptActionInterface
BuildScriptLogAndScriptLogLineExecutionLogger executionLogger = new BuildScriptLogAndScriptLogLineExecutionLogger(null, null);
executeCodeInput.setExecutionLogger(executionLogger);
setupTestScriptInput(input, executeCodeInput);
ExecuteCodeOutput executeCodeOutput = new ExecuteCodeOutput();
try
{
setupTestScriptInput(input, executeCodeInput);
ScriptRevision scriptRevision = new ScriptRevision().withApiName(input.getApiName()).withApiVersion(input.getApiVersion());
if(this instanceof AssociatedScriptContextPrimerInterface associatedScriptContextPrimerInterface)
{
associatedScriptContextPrimerInterface.primeContext(executeCodeInput, scriptRevision);
}
ExecuteCodeOutput executeCodeOutput = new ExecuteCodeOutput();
ExecuteCodeAction.addApiUtilityToContext(executeCodeInput.getContext(), scriptRevision);
new ExecuteCodeAction().run(executeCodeInput, executeCodeOutput);
output.setOutputObject(processTestScriptOutput(executeCodeOutput));
}

View File

@ -76,13 +76,13 @@ public class DeleteAction
QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher();
QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(deleteInput.getBackend());
DeleteInterface deleteInterface = qModule.getDeleteInterface();
if(CollectionUtils.nullSafeHasContents(deleteInput.getPrimaryKeys()) && deleteInput.getQueryFilter() != null)
{
throw (new QException("A delete request may not contain both a list of primary keys and a query filter."));
}
DeleteInterface deleteInterface = qModule.getDeleteInterface();
if(deleteInput.getQueryFilter() != null && !deleteInterface.supportsQueryFilterInput())
{
LOG.info("Querying for primary keys, for backend module " + qModule.getBackendType() + " which does not support queryFilter input for deletes");
@ -99,8 +99,8 @@ public class DeleteAction
}
}
List<QRecord> recordListForAudit = getRecordListForAuditIfNeeded(deleteInput);
List<QRecord> recordsWithValidationErrors = validateRecordsExistAndCanBeAccessed(deleteInput, recordListForAudit);
List<QRecord> recordListForAudit = deleteInterface.supportsPreFetchQuery() ? getRecordListForAuditIfNeeded(deleteInput) : new ArrayList<>();
List<QRecord> recordsWithValidationErrors = deleteInterface.supportsPreFetchQuery() ? validateRecordsExistAndCanBeAccessed(deleteInput, recordListForAudit) : new ArrayList<>();
DeleteOutput deleteOutput = deleteInterface.execute(deleteInput);

View File

@ -35,6 +35,7 @@ import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPostQueryCusto
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
import com.kingsrook.qqq.backend.core.actions.reporting.BufferedRecordPipe;
import com.kingsrook.qqq.backend.core.actions.reporting.RecordPipeBufferedWrapper;
import com.kingsrook.qqq.backend.core.actions.values.QPossibleValueTranslator;
import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter;
import com.kingsrook.qqq.backend.core.context.QContext;
@ -85,14 +86,15 @@ public class QueryAction
if(queryInput.getRecordPipe() != null)
{
queryInput.getRecordPipe().setPostRecordActions(this::postRecordActions);
}
if(queryInput.getIncludeAssociations() && queryInput.getRecordPipe() != null)
{
//////////////////////////////////////////////
// todo - support this in the future maybe? //
//////////////////////////////////////////////
throw (new QException("Associations may not be fetched into a RecordPipe."));
if(queryInput.getIncludeAssociations())
{
//////////////////////////////////////////////////////////////////////////////////////////
// if the user requested to include associations, it's important that that is buffered, //
// (for performance reasons), so, wrap the user's pipe with a buffer //
//////////////////////////////////////////////////////////////////////////////////////////
queryInput.setRecordPipe(new RecordPipeBufferedWrapper(queryInput.getRecordPipe()));
}
}
QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher();
@ -111,11 +113,6 @@ public class QueryAction
postRecordActions(queryOutput.getRecords());
}
if(queryInput.getIncludeAssociations())
{
manageAssociations(queryInput, queryOutput);
}
return queryOutput;
}
@ -124,8 +121,9 @@ public class QueryAction
/*******************************************************************************
**
*******************************************************************************/
private void manageAssociations(QueryInput queryInput, QueryOutput queryOutput) throws QException
private void manageAssociations(QueryInput queryInput, List<QRecord> queryOutputRecords) throws QException
{
LOG.info("In manageAssociations for " + queryInput.getTableName() + " with " + queryOutputRecords.size() + " records");
QTableMetaData table = queryInput.getTable();
for(Association association : CollectionUtils.nonNullList(table.getAssociations()))
{
@ -149,7 +147,7 @@ public class QueryAction
{
JoinOn joinOn = join.getJoinOns().get(0);
Set<Serializable> values = new HashSet<>();
for(QRecord record : queryOutput.getRecords())
for(QRecord record : queryOutputRecords)
{
Serializable value = record.getValue(joinOn.getLeftField());
values.add(value);
@ -161,7 +159,7 @@ public class QueryAction
{
filter.setBooleanOperator(QQueryFilter.BooleanOperator.OR);
for(QRecord record : queryOutput.getRecords())
for(QRecord record : queryOutputRecords)
{
QQueryFilter subFilter = new QQueryFilter();
filter.addSubFilter(subFilter);
@ -229,7 +227,7 @@ public class QueryAction
** not one created via List.of()). This may include setting display values,
** translating possible values, and running post-record customizations.
*******************************************************************************/
public void postRecordActions(List<QRecord> records)
public void postRecordActions(List<QRecord> records) throws QException
{
if(this.postQueryRecordCustomizer.isPresent())
{
@ -250,6 +248,11 @@ public class QueryAction
QValueFormatter.setDisplayValuesInRecords(queryInput.getTable(), records);
}
if(queryInput.getIncludeAssociations())
{
manageAssociations(queryInput, records);
}
//////////////////////////////
// mask any password fields //
//////////////////////////////

View File

@ -33,6 +33,7 @@ import com.kingsrook.qqq.backend.core.actions.ActionHelper;
import com.kingsrook.qqq.backend.core.actions.audits.DMLAuditAction;
import com.kingsrook.qqq.backend.core.actions.automation.AutomationStatus;
import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationStatusUpdater;
import com.kingsrook.qqq.backend.core.actions.interfaces.UpdateInterface;
import com.kingsrook.qqq.backend.core.actions.tables.helpers.ValidateRecordSecurityLockHelper;
import com.kingsrook.qqq.backend.core.actions.values.ValueBehaviorApplier;
import com.kingsrook.qqq.backend.core.context.QContext;
@ -87,18 +88,24 @@ public class UpdateAction
ValueBehaviorApplier.applyFieldBehaviors(updateInput.getInstance(), updateInput.getTable(), updateInput.getRecords());
// todo - need to handle records with errors coming out of here...
List<QRecord> oldRecordList = getOldRecordListForAuditIfNeeded(updateInput);
QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher();
QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(updateInput.getBackend());
UpdateInterface updateInterface = qModule.getUpdateInterface();
List<QRecord> oldRecordList = updateInterface.supportsPreFetchQuery() ? getOldRecordListForAuditIfNeeded(updateInput) : new ArrayList<>();
validatePrimaryKeysAreGiven(updateInput);
validateRecordsExistAndCanBeAccessed(updateInput, oldRecordList);
if(updateInterface.supportsPreFetchQuery())
{
validateRecordsExistAndCanBeAccessed(updateInput, oldRecordList);
}
validateRequiredFields(updateInput);
ValidateRecordSecurityLockHelper.validateSecurityFields(updateInput.getTable(), updateInput.getRecords(), ValidateRecordSecurityLockHelper.Action.UPDATE);
// todo pre-customization - just get to modify the request?
UpdateOutput updateOutput = qModule.getUpdateInterface().execute(updateInput);
UpdateOutput updateOutput = updateInterface.execute(updateInput);
// todo post-customization - can do whatever w/ the result if you want
List<String> errors = updateOutput.getRecords().stream().flatMap(r -> r.getErrors().stream()).toList();

View File

@ -261,7 +261,7 @@ public class ValidateRecordSecurityLockHelper
QSecurityKeyType securityKeyType = QContext.getQInstance().getSecurityKeyType(recordSecurityLock.getSecurityKeyType());
if(StringUtils.hasContent(securityKeyType.getAllAccessKeyName()) && QContext.getQSession().hasSecurityKeyValue(securityKeyType.getAllAccessKeyName(), true, QFieldType.BOOLEAN))
{
LOG.debug("Session has " + securityKeyType.getAllAccessKeyName() + " - not checking this lock.");
LOG.trace("Session has " + securityKeyType.getAllAccessKeyName() + " - not checking this lock.");
}
else
{

View File

@ -31,6 +31,7 @@ import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import com.kingsrook.qqq.backend.core.actions.reporting.RecordPipe;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.shared.mapping.AbstractQFieldMapping;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
@ -61,7 +62,7 @@ public class CsvToQRecordAdapter
** using a given mapping.
**
*******************************************************************************/
public void buildRecordsFromCsv(RecordPipe recordPipe, String csv, QTableMetaData table, AbstractQFieldMapping<?> mapping, Consumer<QRecord> recordCustomizer)
public void buildRecordsFromCsv(RecordPipe recordPipe, String csv, QTableMetaData table, AbstractQFieldMapping<?> mapping, Consumer<QRecord> recordCustomizer) throws QException
{
buildRecordsFromCsv(new InputWrapper().withRecordPipe(recordPipe).withCsv(csv).withTable(table).withMapping(mapping).withRecordCustomizer(recordCustomizer));
}
@ -73,7 +74,7 @@ public class CsvToQRecordAdapter
** using a given mapping.
**
*******************************************************************************/
public List<QRecord> buildRecordsFromCsv(String csv, QTableMetaData table, AbstractQFieldMapping<?> mapping)
public List<QRecord> buildRecordsFromCsv(String csv, QTableMetaData table, AbstractQFieldMapping<?> mapping) throws QException
{
buildRecordsFromCsv(new InputWrapper().withCsv(csv).withTable(table).withMapping(mapping));
return (recordList);
@ -87,7 +88,7 @@ public class CsvToQRecordAdapter
**
** todo - meta-data validation, type handling
*******************************************************************************/
public void buildRecordsFromCsv(InputWrapper inputWrapper)
public void buildRecordsFromCsv(InputWrapper inputWrapper) throws QException
{
String csv = inputWrapper.getCsv();
AbstractQFieldMapping<?> mapping = inputWrapper.getMapping();
@ -297,7 +298,7 @@ public class CsvToQRecordAdapter
/*******************************************************************************
** Add a record - either to the pipe, or list, whichever we're building.
*******************************************************************************/
private void addRecord(QRecord record)
private void addRecord(QRecord record) throws QException
{
if(recordPipe != null)
{

View File

@ -0,0 +1,198 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.actions.scripts;
import java.io.Serializable;
import java.util.Map;
import com.kingsrook.qqq.backend.core.actions.scripts.logging.QCodeExecutionLoggerInterface;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
/*******************************************************************************
** Base class for input wrappers that end up running scripts (ExecuteCodeAction)
*******************************************************************************/
public class AbstractRunScriptInput<C extends QCodeReference> extends AbstractTableActionInput
{
private C codeReference;
private Map<String, Serializable> inputValues;
private QCodeExecutionLoggerInterface logger;
private Serializable outputObject;
private Serializable scriptUtils;
/*******************************************************************************
** Getter for codeReference
*******************************************************************************/
public C getCodeReference()
{
return (this.codeReference);
}
/*******************************************************************************
** Setter for codeReference
*******************************************************************************/
public void setCodeReference(C codeReference)
{
this.codeReference = codeReference;
}
/*******************************************************************************
** Fluent setter for codeReference
*******************************************************************************/
public AbstractRunScriptInput<C> withCodeReference(C codeReference)
{
this.codeReference = codeReference;
return (this);
}
/*******************************************************************************
** Getter for inputValues
*******************************************************************************/
public Map<String, Serializable> getInputValues()
{
return (this.inputValues);
}
/*******************************************************************************
** Setter for inputValues
*******************************************************************************/
public void setInputValues(Map<String, Serializable> inputValues)
{
this.inputValues = inputValues;
}
/*******************************************************************************
** Fluent setter for inputValues
*******************************************************************************/
public AbstractRunScriptInput<C> withInputValues(Map<String, Serializable> inputValues)
{
this.inputValues = inputValues;
return (this);
}
/*******************************************************************************
** Getter for logger
*******************************************************************************/
public QCodeExecutionLoggerInterface getLogger()
{
return (this.logger);
}
/*******************************************************************************
** Setter for logger
*******************************************************************************/
public void setLogger(QCodeExecutionLoggerInterface logger)
{
this.logger = logger;
}
/*******************************************************************************
** Fluent setter for logger
*******************************************************************************/
public AbstractRunScriptInput<C> withLogger(QCodeExecutionLoggerInterface logger)
{
this.logger = logger;
return (this);
}
/*******************************************************************************
** Getter for outputObject
*******************************************************************************/
public Serializable getOutputObject()
{
return (this.outputObject);
}
/*******************************************************************************
** Setter for outputObject
*******************************************************************************/
public void setOutputObject(Serializable outputObject)
{
this.outputObject = outputObject;
}
/*******************************************************************************
** Fluent setter for outputObject
*******************************************************************************/
public AbstractRunScriptInput<C> withOutputObject(Serializable outputObject)
{
this.outputObject = outputObject;
return (this);
}
/*******************************************************************************
** Getter for scriptUtils
*******************************************************************************/
public Serializable getScriptUtils()
{
return (this.scriptUtils);
}
/*******************************************************************************
** Setter for scriptUtils
*******************************************************************************/
public void setScriptUtils(Serializable scriptUtils)
{
this.scriptUtils = scriptUtils;
}
/*******************************************************************************
** Fluent setter for scriptUtils
*******************************************************************************/
public AbstractRunScriptInput<C> withScriptUtils(Serializable scriptUtils)
{
this.scriptUtils = scriptUtils;
return (this);
}
}

View File

@ -24,9 +24,6 @@ package com.kingsrook.qqq.backend.core.model.actions.scripts;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.actions.scripts.logging.QCodeExecutionLoggerInterface;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.code.AdHocScriptCodeReference;
@ -34,18 +31,10 @@ import com.kingsrook.qqq.backend.core.model.metadata.code.AdHocScriptCodeReferen
/*******************************************************************************
**
*******************************************************************************/
public class RunAdHocRecordScriptInput extends AbstractTableActionInput
public class RunAdHocRecordScriptInput extends AbstractRunScriptInput<AdHocScriptCodeReference>
{
private AdHocScriptCodeReference codeReference;
private Map<String, Serializable> inputValues;
private List<Serializable> recordPrimaryKeyList; // can either supply recordList, or recordPrimaryKeyList
private List<QRecord> recordList;
private String tableName;
private QCodeExecutionLoggerInterface logger;
private Serializable outputObject;
private Serializable scriptUtils;
private List<Serializable> recordPrimaryKeyList; // can either supply recordList, or recordPrimaryKeyList
private List<QRecord> recordList;
@ -58,189 +47,6 @@ public class RunAdHocRecordScriptInput extends AbstractTableActionInput
/*******************************************************************************
** Getter for inputValues
**
*******************************************************************************/
public Map<String, Serializable> getInputValues()
{
return inputValues;
}
/*******************************************************************************
** Setter for inputValues
**
*******************************************************************************/
public void setInputValues(Map<String, Serializable> inputValues)
{
this.inputValues = inputValues;
}
/*******************************************************************************
** Fluent setter for inputValues
**
*******************************************************************************/
public RunAdHocRecordScriptInput 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 RunAdHocRecordScriptInput withOutputObject(Serializable outputObject)
{
this.outputObject = outputObject;
return (this);
}
/*******************************************************************************
** Getter for logger
*******************************************************************************/
public QCodeExecutionLoggerInterface getLogger()
{
return (this.logger);
}
/*******************************************************************************
** Setter for logger
*******************************************************************************/
public void setLogger(QCodeExecutionLoggerInterface logger)
{
this.logger = logger;
}
/*******************************************************************************
** Fluent setter for logger
*******************************************************************************/
public RunAdHocRecordScriptInput withLogger(QCodeExecutionLoggerInterface logger)
{
this.logger = logger;
return (this);
}
/*******************************************************************************
** Getter for scriptUtils
**
*******************************************************************************/
public Serializable getScriptUtils()
{
return scriptUtils;
}
/*******************************************************************************
** Setter for scriptUtils
**
*******************************************************************************/
public void setScriptUtils(Serializable scriptUtils)
{
this.scriptUtils = scriptUtils;
}
/*******************************************************************************
** Getter for codeReference
*******************************************************************************/
public AdHocScriptCodeReference getCodeReference()
{
return (this.codeReference);
}
/*******************************************************************************
** Setter for codeReference
*******************************************************************************/
public void setCodeReference(AdHocScriptCodeReference codeReference)
{
this.codeReference = codeReference;
}
/*******************************************************************************
** Fluent setter for codeReference
*******************************************************************************/
public RunAdHocRecordScriptInput withCodeReference(AdHocScriptCodeReference codeReference)
{
this.codeReference = codeReference;
return (this);
}
/*******************************************************************************
** Getter for tableName
*******************************************************************************/
public String getTableName()
{
return (this.tableName);
}
/*******************************************************************************
** Setter for tableName
*******************************************************************************/
public void setTableName(String tableName)
{
this.tableName = tableName;
}
/*******************************************************************************
** Fluent setter for tableName
*******************************************************************************/
public RunAdHocRecordScriptInput withTableName(String tableName)
{
this.tableName = tableName;
return (this);
}
/*******************************************************************************
** Getter for recordList
*******************************************************************************/

View File

@ -22,187 +22,46 @@
package com.kingsrook.qqq.backend.core.model.actions.scripts;
import java.io.Serializable;
import java.util.Map;
import com.kingsrook.qqq.backend.core.actions.scripts.logging.QCodeExecutionLoggerInterface;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
import com.kingsrook.qqq.backend.core.actions.scripts.AssociatedScriptContextPrimerInterface;
import com.kingsrook.qqq.backend.core.model.metadata.code.AssociatedScriptCodeReference;
/*******************************************************************************
**
*******************************************************************************/
public class RunAssociatedScriptInput extends AbstractTableActionInput
public class RunAssociatedScriptInput extends AbstractRunScriptInput<AssociatedScriptCodeReference>
{
private AssociatedScriptCodeReference codeReference;
private Map<String, Serializable> inputValues;
private QCodeExecutionLoggerInterface logger;
private Serializable outputObject;
private Serializable scriptUtils;
private AssociatedScriptContextPrimerInterface associatedScriptContextPrimerInterface;
/*******************************************************************************
**
** Getter for associatedScriptContextPrimerInterface
*******************************************************************************/
public RunAssociatedScriptInput()
public AssociatedScriptContextPrimerInterface getAssociatedScriptContextPrimerInterface()
{
return (this.associatedScriptContextPrimerInterface);
}
/*******************************************************************************
** Getter for codeReference
**
** Setter for associatedScriptContextPrimerInterface
*******************************************************************************/
public AssociatedScriptCodeReference getCodeReference()
public void setAssociatedScriptContextPrimerInterface(AssociatedScriptContextPrimerInterface associatedScriptContextPrimerInterface)
{
return codeReference;
this.associatedScriptContextPrimerInterface = associatedScriptContextPrimerInterface;
}
/*******************************************************************************
** Setter for codeReference
**
** Fluent setter for associatedScriptContextPrimerInterface
*******************************************************************************/
public void setCodeReference(AssociatedScriptCodeReference codeReference)
public RunAssociatedScriptInput withAssociatedScriptContextPrimerInterface(AssociatedScriptContextPrimerInterface associatedScriptContextPrimerInterface)
{
this.codeReference = codeReference;
}
/*******************************************************************************
** Fluent setter for codeReference
**
*******************************************************************************/
public RunAssociatedScriptInput withCodeReference(AssociatedScriptCodeReference codeReference)
{
this.codeReference = codeReference;
this.associatedScriptContextPrimerInterface = associatedScriptContextPrimerInterface;
return (this);
}
/*******************************************************************************
** Getter for inputValues
**
*******************************************************************************/
public Map<String, Serializable> getInputValues()
{
return inputValues;
}
/*******************************************************************************
** Setter for inputValues
**
*******************************************************************************/
public void setInputValues(Map<String, Serializable> inputValues)
{
this.inputValues = inputValues;
}
/*******************************************************************************
** Fluent setter for 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);
}
/*******************************************************************************
** Getter for logger
*******************************************************************************/
public QCodeExecutionLoggerInterface getLogger()
{
return (this.logger);
}
/*******************************************************************************
** Setter for logger
*******************************************************************************/
public void setLogger(QCodeExecutionLoggerInterface logger)
{
this.logger = logger;
}
/*******************************************************************************
** Fluent setter for logger
*******************************************************************************/
public RunAssociatedScriptInput withLogger(QCodeExecutionLoggerInterface logger)
{
this.logger = logger;
return (this);
}
/*******************************************************************************
** Getter for scriptUtils
**
*******************************************************************************/
public Serializable getScriptUtils()
{
return scriptUtils;
}
/*******************************************************************************
** Setter for scriptUtils
**
*******************************************************************************/
public void setScriptUtils(Serializable scriptUtils)
{
this.scriptUtils = scriptUtils;
}
}

View File

@ -35,6 +35,8 @@ public class StoreAssociatedScriptInput extends AbstractTableActionInput
private Serializable recordPrimaryKey;
private String code;
private String apiName;
private String apiVersion;
private String commitMessage;
@ -183,4 +185,66 @@ public class StoreAssociatedScriptInput extends AbstractTableActionInput
return (this);
}
/*******************************************************************************
** Getter for apiName
*******************************************************************************/
public String getApiName()
{
return (this.apiName);
}
/*******************************************************************************
** Setter for apiName
*******************************************************************************/
public void setApiName(String apiName)
{
this.apiName = apiName;
}
/*******************************************************************************
** Fluent setter for apiName
*******************************************************************************/
public StoreAssociatedScriptInput withApiName(String apiName)
{
this.apiName = apiName;
return (this);
}
/*******************************************************************************
** Getter for apiVersion
*******************************************************************************/
public String getApiVersion()
{
return (this.apiVersion);
}
/*******************************************************************************
** Setter for apiVersion
*******************************************************************************/
public void setApiVersion(String apiVersion)
{
this.apiVersion = apiVersion;
}
/*******************************************************************************
** Fluent setter for apiVersion
*******************************************************************************/
public StoreAssociatedScriptInput withApiVersion(String apiVersion)
{
this.apiVersion = apiVersion;
return (this);
}
}

View File

@ -36,6 +36,9 @@ public class TestScriptInput extends AbstractTableActionInput
private Map<String, Serializable> inputValues;
private QCodeReference codeReference;
private String apiName;
private String apiVersion;
/*******************************************************************************
@ -113,4 +116,66 @@ public class TestScriptInput extends AbstractTableActionInput
return (this);
}
/*******************************************************************************
** Getter for apiName
*******************************************************************************/
public String getApiName()
{
return (this.apiName);
}
/*******************************************************************************
** Setter for apiName
*******************************************************************************/
public void setApiName(String apiName)
{
this.apiName = apiName;
}
/*******************************************************************************
** Fluent setter for apiName
*******************************************************************************/
public TestScriptInput withApiName(String apiName)
{
this.apiName = apiName;
return (this);
}
/*******************************************************************************
** Getter for apiVersion
*******************************************************************************/
public String getApiVersion()
{
return (this.apiVersion);
}
/*******************************************************************************
** Setter for apiVersion
*******************************************************************************/
public void setApiVersion(String apiVersion)
{
this.apiVersion = apiVersion;
}
/*******************************************************************************
** Fluent setter for apiVersion
*******************************************************************************/
public TestScriptInput withApiVersion(String apiVersion)
{
this.apiVersion = apiVersion;
return (this);
}
}

View File

@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.query;
import java.io.Serializable;
import java.util.List;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
@ -63,7 +64,7 @@ public class QueryOutput extends AbstractActionOutput implements Serializable
** that could be read asynchronously, at any time, by another thread - SO - only
** completely populated records should be passed into this method.
*******************************************************************************/
public void addRecord(QRecord record)
public void addRecord(QRecord record) throws QException
{
storage.addRecord(record);
}
@ -73,7 +74,7 @@ public class QueryOutput extends AbstractActionOutput implements Serializable
/*******************************************************************************
** add a list of records to this output
*******************************************************************************/
public void addRecords(List<QRecord> records)
public void addRecords(List<QRecord> records) throws QException
{
storage.addRecords(records);
}

View File

@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.query;
import java.util.List;
import com.kingsrook.qqq.backend.core.actions.reporting.RecordPipe;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
@ -53,7 +54,7 @@ class QueryOutputRecordPipe implements QueryOutputStorageInterface
** add a record to this output
*******************************************************************************/
@Override
public void addRecord(QRecord record)
public void addRecord(QRecord record) throws QException
{
recordPipe.addRecord(record);
}
@ -64,7 +65,7 @@ class QueryOutputRecordPipe implements QueryOutputStorageInterface
** add a list of records to this output
*******************************************************************************/
@Override
public void addRecords(List<QRecord> records)
public void addRecords(List<QRecord> records) throws QException
{
recordPipe.addRecords(records);
}

View File

@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.query;
import java.util.List;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
@ -36,13 +37,13 @@ interface QueryOutputStorageInterface
/*******************************************************************************
** add a records to this output
*******************************************************************************/
void addRecord(QRecord record);
void addRecord(QRecord record) throws QException;
/*******************************************************************************
** add a list of records to this output
*******************************************************************************/
void addRecords(List<QRecord> records);
void addRecords(List<QRecord> records) throws QException;
/*******************************************************************************
** Get all stored records

View File

@ -38,6 +38,12 @@ public class TableAutomationAction
private Integer priority = 500;
private QQueryFilter filter;
////////////////////////////////////////////////////////////////////////
// flag that will cause the records to cause their associations to be //
// fetched, when they are looked up for passing into the action //
////////////////////////////////////////////////////////////////////////
private boolean includeRecordAssociations = false;
private Map<String, Serializable> values;
////////////////////////////////
@ -292,4 +298,35 @@ public class TableAutomationAction
return (this);
}
/*******************************************************************************
** Getter for includeRecordAssociations
*******************************************************************************/
public boolean getIncludeRecordAssociations()
{
return (this.includeRecordAssociations);
}
/*******************************************************************************
** Setter for includeRecordAssociations
*******************************************************************************/
public void setIncludeRecordAssociations(boolean includeRecordAssociations)
{
this.includeRecordAssociations = includeRecordAssociations;
}
/*******************************************************************************
** Fluent setter for includeRecordAssociations
*******************************************************************************/
public TableAutomationAction withIncludeRecordAssociations(boolean includeRecordAssociations)
{
this.includeRecordAssociations = includeRecordAssociations;
return (this);
}
}

View File

@ -27,6 +27,7 @@ import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.data.QField;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior;
/*******************************************************************************
@ -48,16 +49,22 @@ public class ScriptRevision extends QRecordEntity
@QField(possibleValueSourceName = "script")
private Integer scriptId;
@QField(possibleValueSourceName = "apiVersion", label = "API Version")
private String apiVersion;
@QField(possibleValueSourceName = "apiName", label = "API Name")
private String apiName;
@QField()
private String contents;
@QField()
private Integer sequenceNo;
@QField()
@QField(maxLength = 250, valueTooLongBehavior = ValueTooLongBehavior.TRUNCATE_ELLIPSIS)
private String commitMessage;
@QField()
@QField(maxLength = 100, valueTooLongBehavior = ValueTooLongBehavior.TRUNCATE_ELLIPSIS)
private String author;
@ -353,4 +360,66 @@ public class ScriptRevision extends QRecordEntity
return (this);
}
/*******************************************************************************
** Getter for apiVersion
*******************************************************************************/
public String getApiVersion()
{
return (this.apiVersion);
}
/*******************************************************************************
** Setter for apiVersion
*******************************************************************************/
public void setApiVersion(String apiVersion)
{
this.apiVersion = apiVersion;
}
/*******************************************************************************
** Fluent setter for apiVersion
*******************************************************************************/
public ScriptRevision withApiVersion(String apiVersion)
{
this.apiVersion = apiVersion;
return (this);
}
/*******************************************************************************
** Getter for apiName
*******************************************************************************/
public String getApiName()
{
return (this.apiName);
}
/*******************************************************************************
** Setter for apiName
*******************************************************************************/
public void setApiName(String apiName)
{
this.apiName = apiName;
}
/*******************************************************************************
** Fluent setter for apiName
*******************************************************************************/
public ScriptRevision withApiName(String apiName)
{
this.apiName = apiName;
return (this);
}
}

View File

@ -389,6 +389,23 @@ public class ScriptsMetaDataProvider
tableMetaData.getField("contents").withFieldAdornment(new FieldAdornment(AdornmentType.CODE_EDITOR).withValue(AdornmentType.CodeEditorValues.languageMode("javascript")));
tableMetaData.getField("scriptId").withFieldAdornment(AdornmentType.Size.LARGE.toAdornment());
try
{
////////////////////////////////////////////////////////////////////////////////////////////////////
// if the api module is loaded, then add a section to the table for the api name & version fields //
////////////////////////////////////////////////////////////////////////////////////////////////////
Class.forName("com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaDataProvider");
tableMetaData.getSections().add(1, new QFieldSection("api", "API", new QIcon().withName("code"), Tier.T2, List.of("apiName", "apiVersion")));
}
catch(ClassNotFoundException e)
{
/////////////////////////////////////////////////////////////////////////////////////////////////////
// if the api module is not loaded, then make sure we don't have these fields in our scripts table //
/////////////////////////////////////////////////////////////////////////////////////////////////////
tableMetaData.getFields().remove("apiName");
tableMetaData.getFields().remove("apiVersion");
}
return (tableMetaData);
}

View File

@ -26,6 +26,7 @@ import com.kingsrook.qqq.backend.core.actions.interfaces.QueryInterface;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
/*******************************************************************************
@ -43,7 +44,16 @@ public class MemoryQueryAction implements QueryInterface
try
{
QueryOutput queryOutput = new QueryOutput(queryInput);
queryOutput.addRecords(MemoryRecordStore.getInstance().query(queryInput));
///////////////////////////////////////////////////////////////////////////////////////////////////////
// add the records to the output one-by-one -- this more closely matches how "real" backends perform //
// and works better w/ pipes //
///////////////////////////////////////////////////////////////////////////////////////////////////////
for(QRecord qRecord : MemoryRecordStore.getInstance().query(queryInput))
{
queryOutput.addRecord(qRecord);
}
return (queryOutput);
}
catch(Exception e)

View File

@ -83,6 +83,9 @@ public class ExtractViaQueryStep extends AbstractExtractStep
queryInput.setRecordPipe(getRecordPipe());
queryInput.setLimit(getLimit());
queryInput.setAsyncJobCallback(runBackendStepInput.getAsyncJobCallback());
customizeInputPreQuery(queryInput);
new QueryAction().execute(queryInput);
///////////////////////////////////////////////////////////////////
@ -92,6 +95,16 @@ public class ExtractViaQueryStep extends AbstractExtractStep
/*******************************************************************************
** chance for sub-classes to change things about the query input, if they want.
*******************************************************************************/
protected void customizeInputPreQuery(QueryInput queryInput)
{
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -28,6 +28,7 @@ import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInpu
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.scripts.Script;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.ExtractViaQueryStep;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
@ -70,4 +71,16 @@ public class RunRecordScriptExtractStep extends ExtractViaQueryStep
super.preRun(runBackendStepInput, runBackendStepOutput);
}
/*******************************************************************************
** Make sure associations are fetched (so api records have children!)
*******************************************************************************/
@Override
protected void customizeInputPreQuery(QueryInput queryInput)
{
super.customizeInputPreQuery(queryInput);
queryInput.setIncludeAssociations(true);
}
}

View File

@ -112,6 +112,8 @@ public class StoreScriptRevisionProcessStep implements BackendStep
QRecord scriptRevision = new QRecord()
.withValue("scriptId", script.getValue("id"))
.withValue("contents", input.getValueString("contents"))
.withValue("apiName", input.getValueString("apiName"))
.withValue("apiVersion", input.getValueString("apiVersion"))
.withValue("commitMessage", commitMessage)
.withValue("sequenceNo", nextSequenceNo);

View File

@ -74,11 +74,12 @@ public class TestScriptProcessStep implements BackendStep
// get inputs //
////////////////
Integer scriptId = input.getValueInteger("scriptId");
String code = input.getValueString("code");
ScriptRevision scriptRevision = new ScriptRevision();
scriptRevision.setScriptId(scriptId);
scriptRevision.setContents(code);
scriptRevision.setContents(input.getValueString("code"));
scriptRevision.setApiName(input.getValueString("apiName"));
scriptRevision.setApiVersion(input.getValueString("apiVersion"));
BuildScriptLogAndScriptLogLineExecutionLogger executionLogger = new BuildScriptLogAndScriptLogLineExecutionLogger(null, null);
@ -106,6 +107,7 @@ public class TestScriptProcessStep implements BackendStep
QueryInput queryInput = new QueryInput();
queryInput.setTableName(tableName);
queryInput.setFilter(new QQueryFilter(new QFilterCriteria(table.getPrimaryKeyField(), QCriteriaOperator.IN, recordPrimaryKeyList.split(","))));
queryInput.setIncludeAssociations(true);
QueryOutput queryOutput = new QueryAction().execute(queryInput);
if(CollectionUtils.nullSafeIsEmpty(queryOutput.getRecords()))
{

View File

@ -26,6 +26,7 @@ import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qqq.backend.core.BaseTest;
import com.kingsrook.qqq.backend.core.actions.async.AsyncRecordPipeLoop;
import com.kingsrook.qqq.backend.core.actions.reporting.RecordPipe;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
@ -189,6 +190,77 @@ class QueryActionTest extends BaseTest
/*******************************************************************************
**
*******************************************************************************/
@Test
void testQueryAssociationsWithPipe() throws QException
{
QContext.getQSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_TYPE_STORE_ALL_ACCESS, true);
insert2OrdersWith3Lines3LineExtrinsicsAnd4OrderExtrinsicAssociations();
RecordPipe pipe = new RecordPipe();
QueryInput queryInput = new QueryInput();
queryInput.setTableName(TestUtils.TABLE_NAME_ORDER);
queryInput.setRecordPipe(pipe);
queryInput.setIncludeAssociations(true);
QueryOutput queryOutput = new QueryAction().execute(queryInput);
assertNotNull(queryOutput);
List<QRecord> records = pipe.consumeAvailableRecords();
assertThat(records).isNotEmpty();
QRecord order0 = records.get(0);
assertEquals(2, order0.getAssociatedRecords().get("orderLine").size());
assertEquals(3, order0.getAssociatedRecords().get("extrinsics").size());
QRecord orderLine00 = order0.getAssociatedRecords().get("orderLine").get(0);
assertEquals(1, orderLine00.getAssociatedRecords().get("extrinsics").size());
QRecord orderLine01 = order0.getAssociatedRecords().get("orderLine").get(1);
assertEquals(2, orderLine01.getAssociatedRecords().get("extrinsics").size());
QRecord order1 = records.get(1);
assertEquals(1, order1.getAssociatedRecords().get("orderLine").size());
assertEquals(1, order1.getAssociatedRecords().get("extrinsics").size());
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testQueryManyRecordsAssociationsWithPipe() throws QException
{
QContext.getQSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_TYPE_STORE_ALL_ACCESS, true);
insertNOrdersWithAssociations(2500);
RecordPipe pipe = new RecordPipe(1000);
QueryInput queryInput = new QueryInput();
queryInput.setTableName(TestUtils.TABLE_NAME_ORDER);
queryInput.setRecordPipe(pipe);
queryInput.setIncludeAssociations(true);
int recordsConsumed = new AsyncRecordPipeLoop().run("Test", null, pipe, (callback) ->
{
new QueryAction().execute(queryInput);
return (true);
}, () ->
{
List<QRecord> records = pipe.consumeAvailableRecords();
for(QRecord record : records)
{
assertEquals(1, record.getAssociatedRecords().get("orderLine").size());
assertEquals(1, record.getAssociatedRecords().get("extrinsics").size());
}
return (records.size());
});
assertEquals(2500, recordsConsumed);
}
/*******************************************************************************
**
*******************************************************************************/
@ -345,4 +417,25 @@ class QueryActionTest extends BaseTest
));
new InsertAction().execute(insertInput);
}
/*******************************************************************************
**
*******************************************************************************/
private static void insertNOrdersWithAssociations(int n) throws QException
{
List<QRecord> recordList = new ArrayList<>();
for(int i = 0; i < n; i++)
{
recordList.add(new QRecord().withValue("storeId", 1).withValue("orderNo", "ORD" + i)
.withAssociatedRecord("orderLine", new QRecord().withValue("sku", "BASIC1").withValue("quantity", 3))
.withAssociatedRecord("extrinsics", new QRecord().withValue("key", "YOUR-FIELD").withValue("value", "YOUR-VALUE")));
}
InsertInput insertInput = new InsertInput();
insertInput.setTableName(TestUtils.TABLE_NAME_ORDER);
insertInput.setRecords(recordList);
new InsertAction().execute(insertInput);
}
}

View File

@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.core.adapters;
import java.util.List;
import com.kingsrook.qqq.backend.core.BaseTest;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.shared.mapping.QIndexBasedFieldMapping;
import com.kingsrook.qqq.backend.core.model.actions.shared.mapping.QKeyBasedFieldMapping;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
@ -48,7 +49,7 @@ class CsvToQRecordAdapterTest extends BaseTest
**
*******************************************************************************/
@Test
public void test_buildRecordsFromCsv_nullInput()
public void test_buildRecordsFromCsv_nullInput() throws QException
{
testExpectedToThrow(null);
}
@ -59,7 +60,7 @@ class CsvToQRecordAdapterTest extends BaseTest
**
*******************************************************************************/
@Test
public void test_buildRecordsFromCsv_emptyStringInput()
public void test_buildRecordsFromCsv_emptyStringInput() throws QException
{
testExpectedToThrow("");
}
@ -69,7 +70,7 @@ class CsvToQRecordAdapterTest extends BaseTest
/*******************************************************************************
**
*******************************************************************************/
private void testExpectedToThrow(String csv)
private void testExpectedToThrow(String csv) throws QException
{
try
{
@ -92,7 +93,7 @@ class CsvToQRecordAdapterTest extends BaseTest
**
*******************************************************************************/
@Test
public void test_buildRecordsFromCsv_emptyList()
public void test_buildRecordsFromCsv_emptyList() throws QException
{
CsvToQRecordAdapter csvToQRecordAdapter = new CsvToQRecordAdapter();
List<QRecord> qRecords = csvToQRecordAdapter.buildRecordsFromCsv(getPersonCsvHeader(), TestUtils.defineTablePerson(), null);
@ -142,7 +143,7 @@ class CsvToQRecordAdapterTest extends BaseTest
**
*******************************************************************************/
@Test
public void test_buildRecordsFromCsv_oneRowStandardHeaderNoMapping()
public void test_buildRecordsFromCsv_oneRowStandardHeaderNoMapping() throws QException
{
CsvToQRecordAdapter csvToQRecordAdapter = new CsvToQRecordAdapter();
List<QRecord> qRecords = csvToQRecordAdapter.buildRecordsFromCsv(getPersonCsvHeader() + getPersonCsvRow1(), TestUtils.defineTablePerson(), null);
@ -159,7 +160,7 @@ class CsvToQRecordAdapterTest extends BaseTest
**
*******************************************************************************/
@Test
public void test_buildRecordsFromCsv_twoRowsStandardHeaderNoMapping()
public void test_buildRecordsFromCsv_twoRowsStandardHeaderNoMapping() throws QException
{
CsvToQRecordAdapter csvToQRecordAdapter = new CsvToQRecordAdapter();
List<QRecord> qRecords = csvToQRecordAdapter.buildRecordsFromCsv(getPersonCsvHeader() + getPersonCsvRow1() + getPersonCsvRow2(), TestUtils.defineTablePerson(), null);
@ -179,7 +180,7 @@ class CsvToQRecordAdapterTest extends BaseTest
**
*******************************************************************************/
@Test
public void test_buildRecordsFromCsv_oneRowCustomKeyBasedMapping()
public void test_buildRecordsFromCsv_oneRowCustomKeyBasedMapping() throws QException
{
String csvCustomHeader = """
"id","created","modified","first","last","birthday","email"\r
@ -209,7 +210,7 @@ class CsvToQRecordAdapterTest extends BaseTest
**
*******************************************************************************/
@Test
public void test_buildRecordsFromCsv_twoRowsCustomIndexBasedMapping()
public void test_buildRecordsFromCsv_twoRowsCustomIndexBasedMapping() throws QException
{
int index = 1;
QIndexBasedFieldMapping mapping = new QIndexBasedFieldMapping()
@ -241,7 +242,7 @@ class CsvToQRecordAdapterTest extends BaseTest
** header names on the RHS.
*******************************************************************************/
@Test
public void test_duplicatedColumnHeaders()
public void test_duplicatedColumnHeaders() throws QException
{
QKeyBasedFieldMapping mapping = new QKeyBasedFieldMapping()
.withMapping("id", "id")
@ -291,7 +292,7 @@ class CsvToQRecordAdapterTest extends BaseTest
**
*******************************************************************************/
@Test
void testByteOrderMarker()
void testByteOrderMarker() throws QException
{
CsvToQRecordAdapter csvToQRecordAdapter = new CsvToQRecordAdapter();
@ -313,7 +314,7 @@ class CsvToQRecordAdapterTest extends BaseTest
** Fix an IndexOutOfBounds that we used to throw.
*******************************************************************************/
@Test
void testTooFewBodyColumns()
void testTooFewBodyColumns() throws QException
{
CsvToQRecordAdapter csvToQRecordAdapter = new CsvToQRecordAdapter();
List<QRecord> records = csvToQRecordAdapter.buildRecordsFromCsv("""
@ -331,7 +332,7 @@ class CsvToQRecordAdapterTest extends BaseTest
**
*******************************************************************************/
@Test
public void testTooFewColumnsIndexMapping()
public void testTooFewColumnsIndexMapping() throws QException
{
int index = 1;
QIndexBasedFieldMapping mapping = new QIndexBasedFieldMapping()
@ -353,7 +354,7 @@ class CsvToQRecordAdapterTest extends BaseTest
**
*******************************************************************************/
@Test
void testCaseSensitiveHeaders()
void testCaseSensitiveHeaders() throws QException
{
CsvToQRecordAdapter csvToQRecordAdapter = new CsvToQRecordAdapter();
csvToQRecordAdapter.buildRecordsFromCsv(new CsvToQRecordAdapter.InputWrapper()
@ -376,7 +377,7 @@ class CsvToQRecordAdapterTest extends BaseTest
**
*******************************************************************************/
@Test
void testCaseInsensitiveHeaders()
void testCaseInsensitiveHeaders() throws QException
{
CsvToQRecordAdapter csvToQRecordAdapter = new CsvToQRecordAdapter();
csvToQRecordAdapter.buildRecordsFromCsv(new CsvToQRecordAdapter.InputWrapper()

View File

@ -222,7 +222,10 @@ class TableSyncProcessTest extends BaseTest
public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
{
List<QRecord> qRecords = TestUtils.queryTable(QContext.getQInstance(), TestUtils.TABLE_NAME_PERSON_MEMORY);
qRecords.forEach(r -> getRecordPipe().addRecord(r));
for(QRecord qRecord : qRecords)
{
getRecordPipe().addRecord(qRecord);
}
////////////////////////////////////////
// re-add records 1 and 5 to the pipe //

View File

@ -34,10 +34,6 @@
<properties>
<!-- props specifically to this module -->
<!-- none at this time -->
<!-- todo - remove this once module is further built out and we can hit standard ratio -->
<coverage.instructionCoveredRatioMinimum>0.00</coverage.instructionCoveredRatioMinimum>
<coverage.classCoveredRatioMinimum>0.00</coverage.classCoveredRatioMinimum>
</properties>
<dependencies>

View File

@ -45,4 +45,16 @@ public class APIUpdateAction extends AbstractAPIAction implements UpdateInterfac
return (apiActionUtil.doUpdate(table, updateInput));
}
/*******************************************************************************
** Specify whether this particular module's update action can & should fetch
** records before updating them, e.g., for audits or "not-found-checks"
*******************************************************************************/
@Override
public boolean supportsPreFetchQuery()
{
return (false);
}
}

View File

@ -634,7 +634,7 @@ public class BaseAPIActionUtil
request.setEntity(new StringEntity(postBody));
request.addHeader("Content-Type", "application/x-www-form-urlencoded; charset=utf-8");
HttpResponse response = client.execute(request);
HttpResponse response = executeOAuthTokenRequest(client, request);
int statusCode = response.getStatusLine().getStatusCode();
HttpEntity entity = response.getEntity();
String resultString = EntityUtils.toString(entity);
@ -669,6 +669,16 @@ public class BaseAPIActionUtil
/*******************************************************************************
** one-line method, factored out so mock/tests can override
*******************************************************************************/
protected CloseableHttpResponse executeOAuthTokenRequest(CloseableHttpClient client, HttpPost request) throws IOException
{
return client.execute(request);
}
/*******************************************************************************
** As part of making a request - set up its content-type header.
*******************************************************************************/
@ -880,7 +890,7 @@ public class BaseAPIActionUtil
LOG.info("POST contents [" + ((HttpPost) request).getEntity().toString() + "]");
}
try(CloseableHttpResponse response = httpClient.execute(request))
try(CloseableHttpResponse response = executeHttpRequest(request, httpClient))
{
QHttpResponse qResponse = new QHttpResponse(response);
@ -924,7 +934,7 @@ public class BaseAPIActionUtil
rateLimitsCaught++;
if(rateLimitsCaught > getMaxAllowedRateLimitErrors())
{
LOG.error("Giving up POST to [" + table.getName() + "] after too many rate-limit errors (" + getMaxAllowedRateLimitErrors() + ")");
LOG.error("Giving up " + request.getMethod() + " to [" + table.getName() + "] after too many rate-limit errors (" + getMaxAllowedRateLimitErrors() + ")");
throw (new QException(rle));
}
@ -950,6 +960,16 @@ public class BaseAPIActionUtil
/*******************************************************************************
** one-line method, factored out so mock/tests can override
*******************************************************************************/
protected CloseableHttpResponse executeHttpRequest(HttpRequestBase request, CloseableHttpClient httpClient) throws IOException
{
return httpClient.execute(request);
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -32,6 +32,8 @@ import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeUsage;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryBackendModule;
import com.kingsrook.qqq.backend.module.api.mocks.MockApiActionUtils;
import com.kingsrook.qqq.backend.module.api.model.AuthorizationType;
import com.kingsrook.qqq.backend.module.api.model.metadata.APIBackendMetaData;
import com.kingsrook.qqq.backend.module.api.model.metadata.APITableBackendDetails;
@ -42,7 +44,10 @@ import com.kingsrook.qqq.backend.module.api.model.metadata.APITableBackendDetail
*******************************************************************************/
public class TestUtils
{
public static final String MEMORY_BACKEND_NAME = "memory";
public static final String EASYPOST_BACKEND_NAME = "easypost";
public static final String MOCK_BACKEND_NAME = "mock";
public static final String MOCK_TABLE_NAME = "mock";
@ -52,14 +57,69 @@ public class TestUtils
public static QInstance defineInstance()
{
QInstance qInstance = new QInstance();
qInstance.addBackend(defineBackend());
qInstance.addTable(defineTableEasypostTracker());
qInstance.setAuthentication(defineAuthentication());
qInstance.addBackend(defineMemoryBackend());
qInstance.addBackend(defineMockBackend());
qInstance.addTable(defineMockTable());
qInstance.addBackend(defineEasypostBackend());
qInstance.addTable(defineTableEasypostTracker());
return (qInstance);
}
/*******************************************************************************
** Define the in-memory backend used in standard tests
*******************************************************************************/
public static QBackendMetaData defineMemoryBackend()
{
return new QBackendMetaData()
.withName(MEMORY_BACKEND_NAME)
.withBackendType(MemoryBackendModule.class);
}
/*******************************************************************************
**
*******************************************************************************/
private static QBackendMetaData defineMockBackend()
{
return (new APIBackendMetaData()
.withName(MOCK_BACKEND_NAME)
.withAuthorizationType(AuthorizationType.API_KEY_HEADER)
.withBaseUrl("http://localhost:9999/mock")
.withContentType("application/json")
.withActionUtil(new QCodeReference(MockApiActionUtils.class, QCodeUsage.CUSTOMIZER))
);
}
/*******************************************************************************
**
*******************************************************************************/
private static QTableMetaData defineMockTable()
{
return (new QTableMetaData()
.withName(MOCK_TABLE_NAME)
.withBackendName(MOCK_BACKEND_NAME)
.withField(new QFieldMetaData("id", QFieldType.STRING))
.withField(new QFieldMetaData("name", QFieldType.STRING))
.withPrimaryKeyField("id")
.withBackendDetails(new APITableBackendDetails()
.withTablePath("mock")
.withTableWrapperObjectName("mocks")
)
);
}
/*******************************************************************************
** Define the authentication used in standard tests - using 'mock' type.
**
@ -76,7 +136,7 @@ public class TestUtils
/*******************************************************************************
**
*******************************************************************************/
public static QBackendMetaData defineBackend()
public static QBackendMetaData defineEasypostBackend()
{
String apiKey = new QMetaDataVariableInterpreter().interpret("${env.EASYPOST_API_KEY}");

View File

@ -0,0 +1,685 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.module.api.actions;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import com.kingsrook.qqq.backend.core.actions.tables.CountAction;
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.SleepUtils;
import com.kingsrook.qqq.backend.module.api.BaseTest;
import com.kingsrook.qqq.backend.module.api.TestUtils;
import com.kingsrook.qqq.backend.module.api.exceptions.RateLimitException;
import com.kingsrook.qqq.backend.module.api.mocks.MockApiActionUtils;
import com.kingsrook.qqq.backend.module.api.mocks.MockApiUtilsHelper;
import com.kingsrook.qqq.backend.module.api.model.AuthorizationType;
import com.kingsrook.qqq.backend.module.api.model.OutboundAPILog;
import com.kingsrook.qqq.backend.module.api.model.OutboundAPILogMetaDataProvider;
import com.kingsrook.qqq.backend.module.api.model.metadata.APIBackendMetaData;
import org.apache.http.Header;
import org.json.JSONArray;
import org.json.JSONObject;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
/*******************************************************************************
** Unit test for com.kingsrook.qqq.backend.module.api.actions.BaseAPIActionUtil
*******************************************************************************/
class BaseAPIActionUtilTest extends BaseTest
{
private static MockApiUtilsHelper mockApiUtilsHelper = new MockApiUtilsHelper();
/*******************************************************************************
**
*******************************************************************************/
@BeforeEach
void beforeEach()
{
mockApiUtilsHelper = new MockApiUtilsHelper();
mockApiUtilsHelper.setUseMock(true);
MockApiActionUtils.mockApiUtilsHelper = mockApiUtilsHelper;
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testCount() throws QException
{
mockApiUtilsHelper.enqueueMockResponse("""
[
{"id": 1, "name": "Homer"},
{"id": 2, "name": "Marge"},
{"id": 3, "name": "Bart"},
{"id": 4, "name": "Lisa"},
{"id": 5, "name": "Maggie"}
]
""");
CountInput countInput = new CountInput();
countInput.setTableName(TestUtils.MOCK_TABLE_NAME);
CountOutput countOutput = new CountAction().execute(countInput);
assertEquals(5, countOutput.getCount());
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testCountError() throws QException
{
////////////////////////////////////////
// avoid the fully mocked makeRequest //
////////////////////////////////////////
mockApiUtilsHelper.setUseMock(false);
mockApiUtilsHelper.enqueueMockResponse(new QHttpResponse().withStatusCode(500).withContent("""
{"error": "Server error"}
"""));
CountInput countInput = new CountInput();
countInput.setTableName(TestUtils.MOCK_TABLE_NAME);
assertThatThrownBy(() -> new CountAction().execute(countInput)).hasRootCauseInstanceOf(Exception.class);
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testGet() throws QException
{
mockApiUtilsHelper.enqueueMockResponse("""
{"id": 3, "name": "Bart"},
""");
GetInput getInput = new GetInput();
getInput.setTableName(TestUtils.MOCK_TABLE_NAME);
getInput.setPrimaryKey(3);
GetOutput getOutput = new GetAction().execute(getInput);
assertEquals(3, getOutput.getRecord().getValueInteger("id"));
assertEquals("Bart", getOutput.getRecord().getValueString("name"));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testGetByKey() throws QException
{
QContext.getQInstance().getTable(TestUtils.MOCK_TABLE_NAME).withUniqueKey(new UniqueKey("id"));
mockApiUtilsHelper.enqueueMockResponse("""
{"id": 3, "name": "Bart"},
""");
GetInput getInput = new GetInput();
getInput.setTableName(TestUtils.MOCK_TABLE_NAME);
getInput.setUniqueKey(Map.of("id", 3));
GetOutput getOutput = new GetAction().execute(getInput);
assertEquals(3, getOutput.getRecord().getValueInteger("id"));
assertEquals("Bart", getOutput.getRecord().getValueString("name"));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testQuery() throws QException
{
mockApiUtilsHelper.enqueueMockResponse("""
[
{"id": 1, "name": "Homer"},
{"id": 2, "name": "Marge"},
{"id": 3, "name": "Bart"},
{"id": 4, "name": "Lisa"},
{"id": 5, "name": "Maggie"}
]
""");
QueryInput queryInput = new QueryInput();
queryInput.setTableName(TestUtils.MOCK_TABLE_NAME);
QueryOutput queryOutput = new QueryAction().execute(queryInput);
assertEquals(5, queryOutput.getRecords().size());
assertEquals(1, queryOutput.getRecords().get(0).getValueInteger("id"));
assertEquals("Homer", queryOutput.getRecords().get(0).getValueString("name"));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testQueryObjectWrappingList() throws QException
{
mockApiUtilsHelper.enqueueMockResponse("""
{"mocks": [
{"id": 1, "name": "Homer"},
{"id": 2, "name": "Marge"},
{"id": 3, "name": "Bart"},
{"id": 4, "name": "Lisa"},
{"id": 5, "name": "Maggie"}
]}
""");
QueryInput queryInput = new QueryInput();
queryInput.setTableName(TestUtils.MOCK_TABLE_NAME);
QueryOutput queryOutput = new QueryAction().execute(queryInput);
assertEquals(5, queryOutput.getRecords().size());
assertEquals(1, queryOutput.getRecords().get(0).getValueInteger("id"));
assertEquals("Homer", queryOutput.getRecords().get(0).getValueString("name"));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testQueryObjectWrappingSingleObject() throws QException
{
mockApiUtilsHelper.enqueueMockResponse("""
{"mocks":
{"id": 1, "name": "Homer"}
}
""");
QueryInput queryInput = new QueryInput();
queryInput.setTableName(TestUtils.MOCK_TABLE_NAME);
QueryOutput queryOutput = new QueryAction().execute(queryInput);
assertEquals(1, queryOutput.getRecords().size());
assertEquals(1, queryOutput.getRecords().get(0).getValueInteger("id"));
assertEquals("Homer", queryOutput.getRecords().get(0).getValueString("name"));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testQueryPaginate() throws QException
{
String oneObject = """
{"id": 1, "name": "Homer"}
""";
StringBuilder response = new StringBuilder("[");
for(int i = 0; i < 19; i++)
{
response.append(oneObject).append(",");
}
response.append(oneObject);
response.append("]");
mockApiUtilsHelper.enqueueMockResponse(response.toString());
mockApiUtilsHelper.enqueueMockResponse(response.toString());
mockApiUtilsHelper.enqueueMockResponse(response.toString());
mockApiUtilsHelper.enqueueMockResponse("[]");
QueryInput queryInput = new QueryInput();
queryInput.setTableName(TestUtils.MOCK_TABLE_NAME);
QueryOutput queryOutput = new QueryAction().execute(queryInput);
assertEquals(60, queryOutput.getRecords().size());
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testQueryError() throws QException
{
////////////////////////////////////////
// avoid the fully mocked makeRequest //
////////////////////////////////////////
mockApiUtilsHelper.setUseMock(false);
mockApiUtilsHelper.enqueueMockResponse(new QHttpResponse().withStatusCode(500).withContent("""
{"error": "Server error"}
"""));
QueryInput queryInput = new QueryInput();
queryInput.setTableName(TestUtils.MOCK_TABLE_NAME);
assertThatThrownBy(() -> new QueryAction().execute(queryInput)).hasRootCauseInstanceOf(Exception.class);
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testInsert() throws QException
{
mockApiUtilsHelper.enqueueMockResponse("""
{"id": 6}
""");
InsertInput insertInput = new InsertInput();
insertInput.setTableName(TestUtils.MOCK_TABLE_NAME);
insertInput.setRecords(List.of(new QRecord().withValue("name", "Milhouse")));
InsertOutput insertOutput = new InsertAction().execute(insertInput);
assertEquals(6, insertOutput.getRecords().get(0).getValueInteger("id"));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testInsertEmptyInputList() throws QException
{
InsertInput insertInput = new InsertInput();
insertInput.setTableName(TestUtils.MOCK_TABLE_NAME);
insertInput.setRecords(List.of());
InsertOutput insertOutput = new InsertAction().execute(insertInput);
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testInsertError() throws QException
{
////////////////////////////////////////
// avoid the fully mocked makeRequest //
////////////////////////////////////////
mockApiUtilsHelper.setUseMock(false);
mockApiUtilsHelper.enqueueMockResponse(new QHttpResponse().withStatusCode(500).withContent("""
{"error": "Server error"}
"""));
InsertInput insertInput = new InsertInput();
insertInput.setRecords(List.of(new QRecord().withValue("name", "Milhouse")));
insertInput.setTableName(TestUtils.MOCK_TABLE_NAME);
InsertOutput insertOutput = new InsertAction().execute(insertInput);
assertTrue(CollectionUtils.nullSafeHasContents(insertOutput.getRecords().get(0).getErrors()));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testUpdate() throws QException
{
mockApiUtilsHelper.enqueueMockResponse("");
mockApiUtilsHelper.setMockRequestAsserter(httpRequestBase ->
{
String requestBody = MockApiUtilsHelper.readRequestBody(httpRequestBase);
JSONObject requestObject = new JSONObject(requestBody);
JSONArray mocks = requestObject.getJSONArray("mocks");
JSONObject record = mocks.getJSONObject(0);
assertEquals("Bartholomew", record.getString("name"));
assertEquals(3, record.getInt("id"));
});
UpdateInput updateInput = new UpdateInput();
updateInput.setTableName(TestUtils.MOCK_TABLE_NAME);
updateInput.setRecords(List.of(new QRecord().withValue("id", "3").withValue("name", "Bartholomew")));
UpdateOutput updateOutput = new UpdateAction().execute(updateInput);
// not sure what to assert in here...
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testUpdateEmptyInputList() throws QException
{
UpdateInput updateInput = new UpdateInput();
updateInput.setTableName(TestUtils.MOCK_TABLE_NAME);
updateInput.setRecords(List.of());
UpdateOutput updateOutput = new UpdateAction().execute(updateInput);
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testUpdateError() throws QException
{
////////////////////////////////////////
// avoid the fully mocked makeRequest //
////////////////////////////////////////
mockApiUtilsHelper.setUseMock(false);
mockApiUtilsHelper.enqueueMockResponse(new QHttpResponse().withStatusCode(500).withContent("""
{"error": "Server error"}
"""));
UpdateInput updateInput = new UpdateInput();
updateInput.setRecords(List.of(new QRecord().withValue("name", "Milhouse")));
updateInput.setTableName(TestUtils.MOCK_TABLE_NAME);
/////////////////////////////////////////////////////////////////////////////////
// note - right now this is inconsistent with insertAction (and rdbms update), //
// where errors are placed in the records, rather than thrown... //
/////////////////////////////////////////////////////////////////////////////////
assertThatThrownBy(() -> new UpdateAction().execute(updateInput)).hasRootCauseInstanceOf(Exception.class);
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testMakeRequest() throws QException
{
////////////////////////////////////////////////////////////////////////////////////////////
// this will make it not use the mock makeRequest method, //
// but instead the mock executeHttpRequest, so we can test code from the base makeRequest //
////////////////////////////////////////////////////////////////////////////////////////////
mockApiUtilsHelper.setUseMock(false);
mockApiUtilsHelper.enqueueMockResponse("""
{"id": 3, "name": "Bart"},
""");
GetInput getInput = new GetInput();
getInput.setTableName(TestUtils.MOCK_TABLE_NAME);
getInput.setPrimaryKey(3);
GetOutput getOutput = new GetAction().execute(getInput);
assertEquals(3, getOutput.getRecord().getValueInteger("id"));
assertEquals("Bart", getOutput.getRecord().getValueString("name"));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void test429Then200() throws QException
{
////////////////////////////////////////////////////////////////////////////////////////////
// this will make it not use the mock makeRequest method, //
// but instead the mock executeHttpRequest, so we can test code from the base makeRequest //
// specifically, that we can get one 429, and then eventually a 200 //
////////////////////////////////////////////////////////////////////////////////////////////
mockApiUtilsHelper.setUseMock(false);
mockApiUtilsHelper.enqueueMockResponse(new QHttpResponse().withStatusCode(429).withContent("Try again"));
mockApiUtilsHelper.enqueueMockResponse("""
{"id": 3, "name": "Bart"},
""");
GetInput getInput = new GetInput();
getInput.setTableName(TestUtils.MOCK_TABLE_NAME);
getInput.setPrimaryKey(3);
GetOutput getOutput = new GetAction().execute(getInput);
assertEquals(3, getOutput.getRecord().getValueInteger("id"));
assertEquals("Bart", getOutput.getRecord().getValueString("name"));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testTooMany429() throws QException
{
////////////////////////////////////////////////////////////////////////////////////////////
// this will make it not use the mock makeRequest method, //
// but instead the mock executeHttpRequest, so we can test code from the base makeRequest //
// specifically, that after too many 429's we get an error //
////////////////////////////////////////////////////////////////////////////////////////////
mockApiUtilsHelper.setUseMock(false);
mockApiUtilsHelper.enqueueMockResponse(new QHttpResponse().withStatusCode(429).withContent("Try again"));
mockApiUtilsHelper.enqueueMockResponse(new QHttpResponse().withStatusCode(429).withContent("Try again"));
mockApiUtilsHelper.enqueueMockResponse(new QHttpResponse().withStatusCode(429).withContent("Try again"));
mockApiUtilsHelper.enqueueMockResponse(new QHttpResponse().withStatusCode(429).withContent("Try again"));
GetInput getInput = new GetInput();
getInput.setTableName(TestUtils.MOCK_TABLE_NAME);
getInput.setPrimaryKey(3);
assertThatThrownBy(() -> new GetAction().execute(getInput)).hasRootCauseInstanceOf(RateLimitException.class);
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testApiLogs() throws QException
{
QInstance qInstance = QContext.getQInstance();
OutboundAPILogMetaDataProvider.defineAll(qInstance, TestUtils.MEMORY_BACKEND_NAME, null);
mockApiUtilsHelper.setUseMock(false);
mockApiUtilsHelper.enqueueMockResponse("""
{"id": 6}
""");
InsertInput insertInput = new InsertInput();
insertInput.setTableName(TestUtils.MOCK_TABLE_NAME);
insertInput.setRecords(List.of(new QRecord().withValue("name", "Milhouse")));
InsertOutput insertOutput = new InsertAction().execute(insertInput);
assertEquals(6, insertOutput.getRecords().get(0).getValueInteger("id"));
//////////////////////////////////////////////////////////////////////////////////////////
// the outbound api log is inserted async, so... do or do not, and sleep some if needed //
//////////////////////////////////////////////////////////////////////////////////////////
QueryOutput apiLogRecords = null;
int tries = 0;
do
{
SleepUtils.sleep(10, TimeUnit.MILLISECONDS);
QueryInput queryInput = new QueryInput();
queryInput.setTableName(OutboundAPILog.TABLE_NAME);
apiLogRecords = new QueryAction().execute(queryInput);
}
while(apiLogRecords.getRecords().isEmpty() && tries++ < 10);
assertEquals(1, apiLogRecords.getRecords().size());
assertEquals("POST", apiLogRecords.getRecords().get(0).getValueString("method"));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testBasicAuthApiKey() throws QException
{
APIBackendMetaData backend = (APIBackendMetaData) QContext.getQInstance().getBackend(TestUtils.MOCK_BACKEND_NAME);
backend.setAuthorizationType(AuthorizationType.BASIC_AUTH_API_KEY);
backend.setApiKey("9876-WXYZ");
////////////////////////////////////////////////////////////////////////////////////////////
// this will make it not use the mock makeRequest method, //
// but instead the mock executeHttpRequest, so we can test code from the base makeRequest //
////////////////////////////////////////////////////////////////////////////////////////////
mockApiUtilsHelper.setUseMock(false);
mockApiUtilsHelper.enqueueMockResponse("""
{"id": 3, "name": "Bart"},
""");
mockApiUtilsHelper.setMockRequestAsserter(request ->
{
Header authHeader = request.getFirstHeader("Authorization");
assertTrue(authHeader.getValue().startsWith("Basic "));
String apiKey = new String(Base64.getDecoder().decode(authHeader.getValue().replace("Basic ", "")), StandardCharsets.UTF_8);
assertEquals("9876-WXYZ", apiKey);
});
runSimpleGetAction();
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testBasicAuthUsernamePassword() throws QException
{
APIBackendMetaData backend = (APIBackendMetaData) QContext.getQInstance().getBackend(TestUtils.MOCK_BACKEND_NAME);
backend.setAuthorizationType(AuthorizationType.BASIC_AUTH_USERNAME_PASSWORD);
backend.setUsername("god");
backend.setPassword("5fingers");
////////////////////////////////////////////////////////////////////////////////////////////
// this will make it not use the mock makeRequest method, //
// but instead the mock executeHttpRequest, so we can test code from the base makeRequest //
////////////////////////////////////////////////////////////////////////////////////////////
mockApiUtilsHelper.setUseMock(false);
mockApiUtilsHelper.enqueueMockResponse("""
{"id": 3, "name": "Bart"},
""");
mockApiUtilsHelper.setMockRequestAsserter(request ->
{
Header authHeader = request.getFirstHeader("Authorization");
assertTrue(authHeader.getValue().startsWith("Basic "));
String usernamePassword = new String(Base64.getDecoder().decode(authHeader.getValue().replace("Basic ", "")), StandardCharsets.UTF_8);
assertEquals("god:5fingers", usernamePassword);
});
runSimpleGetAction();
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testOAuth2ValidToken() throws QException
{
APIBackendMetaData backend = (APIBackendMetaData) QContext.getQInstance().getBackend(TestUtils.MOCK_BACKEND_NAME);
backend.setAuthorizationType(AuthorizationType.OAUTH2);
backend.withCustomValue("accessToken", "validToken");
////////////////////////////////////////////////////////////////////////////////////////////
// this will make it not use the mock makeRequest method, //
// but instead the mock executeHttpRequest, so we can test code from the base makeRequest //
////////////////////////////////////////////////////////////////////////////////////////////
mockApiUtilsHelper.setUseMock(false);
mockApiUtilsHelper.enqueueMockResponse("""
{"id": 3, "name": "Bart"},
""");
mockApiUtilsHelper.setMockRequestAsserter(request ->
{
Header authHeader = request.getFirstHeader("Authorization");
assertTrue(authHeader.getValue().startsWith("Bearer "));
String token = authHeader.getValue().replace("Bearer ", "");
assertEquals("validToken", token);
});
runSimpleGetAction();
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testOAuth2NullToken() throws QException
{
APIBackendMetaData backend = (APIBackendMetaData) QContext.getQInstance().getBackend(TestUtils.MOCK_BACKEND_NAME);
backend.setAuthorizationType(AuthorizationType.OAUTH2);
////////////////////////////////////////////////////////////////////////////////////////////
// this will make it not use the mock makeRequest method, //
// but instead the mock executeHttpRequest, so we can test code from the base makeRequest //
////////////////////////////////////////////////////////////////////////////////////////////
mockApiUtilsHelper.setUseMock(false);
mockApiUtilsHelper.enqueueMockResponse("""
{"access_token": "myNewToken"}
""");
mockApiUtilsHelper.enqueueMockResponse("""
{"id": 3, "name": "Bart"},
""");
GetOutput getOutput = runSimpleGetAction();
assertEquals(3, getOutput.getRecord().getValueInteger("id"));
assertEquals("Bart", getOutput.getRecord().getValueString("name"));
}
/*******************************************************************************
**
*******************************************************************************/
private static GetOutput runSimpleGetAction() throws QException
{
GetInput getInput = new GetInput();
getInput.setTableName(TestUtils.MOCK_TABLE_NAME);
getInput.setPrimaryKey(3);
return (new GetAction().execute(getInput));
}
}

View File

@ -0,0 +1,108 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.module.api.mocks;
import java.io.IOException;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.module.api.actions.BaseAPIActionUtil;
import com.kingsrook.qqq.backend.module.api.actions.QHttpResponse;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.impl.client.CloseableHttpClient;
/*******************************************************************************
**
*******************************************************************************/
public class MockApiActionUtils extends BaseAPIActionUtil
{
public static MockApiUtilsHelper mockApiUtilsHelper;
/*******************************************************************************
**
*******************************************************************************/
@Override
public QHttpResponse makeRequest(QTableMetaData table, HttpRequestBase request) throws QException
{
return (mockApiUtilsHelper.defaultMockMakeRequest(mockApiUtilsHelper, table, request, () -> super.makeRequest(table, request)));
}
/*******************************************************************************
**
*******************************************************************************/
@Override
protected CloseableHttpResponse executeHttpRequest(HttpRequestBase request, CloseableHttpClient httpClient) throws IOException
{
runMockAsserter(request);
return new MockHttpResponse(mockApiUtilsHelper);
}
/*******************************************************************************
**
*******************************************************************************/
private static void runMockAsserter(HttpRequestBase request)
{
if(mockApiUtilsHelper.getMockRequestAsserter() != null)
{
try
{
mockApiUtilsHelper.getMockRequestAsserter().run(request);
}
catch(Exception e)
{
throw (new RuntimeException("Error running mock request asserter", e));
}
}
}
/*******************************************************************************
**
*******************************************************************************/
@Override
protected CloseableHttpResponse executeOAuthTokenRequest(CloseableHttpClient client, HttpPost request) throws IOException
{
runMockAsserter(request);
return new MockHttpResponse(mockApiUtilsHelper);
}
/*******************************************************************************
**
*******************************************************************************/
protected int getInitialRateLimitBackoffMillis()
{
return (1);
}
}

View File

@ -0,0 +1,226 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.module.api.mocks;
import java.io.IOException;
import java.util.ArrayDeque;
import java.util.Deque;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.lambdas.UnsafeConsumer;
import com.kingsrook.qqq.backend.core.utils.lambdas.UnsafeSupplier;
import com.kingsrook.qqq.backend.module.api.actions.QHttpResponse;
import org.apache.commons.io.IOUtils;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpRequestBase;
import static org.junit.jupiter.api.Assertions.fail;
/*******************************************************************************
**
*******************************************************************************/
public class MockApiUtilsHelper
{
private static final QLogger LOG = QLogger.getLogger(MockApiUtilsHelper.class);
private boolean useMock = true;
private Deque<QHttpResponse> mockResponseQueue = new ArrayDeque<>();
private UnsafeConsumer<HttpRequestBase, ? extends Throwable> mockRequestAsserter = null;
/*******************************************************************************
**
*******************************************************************************/
public void enqueueMockResponse(String json)
{
mockResponseQueue.addLast(new QHttpResponse()
.withStatusCode(200)
.withContent(json)
);
}
/*******************************************************************************
**
*******************************************************************************/
public void enqueueMockResponse(QHttpResponse qHttpResponse)
{
mockResponseQueue.addLast(qHttpResponse);
}
/*******************************************************************************
**
*******************************************************************************/
public QHttpResponse returnMockResponseFromQueue(HttpRequestBase request) throws QException
{
if(getMockRequestAsserter() != null)
{
try
{
getMockRequestAsserter().run(request);
}
catch(Exception e)
{
throw (new QException("Error running mock request asserter", e));
}
}
if(mockResponseQueue.isEmpty())
{
fail("No mock response is in the queue for " + request.getMethod() + " " + request.getURI());
}
LOG.info("Returning mock http response for " + request.getMethod() + " " + request.getURI());
return (mockResponseQueue.removeFirst());
}
/*******************************************************************************
** Getter for useMock
*******************************************************************************/
public boolean getUseMock()
{
return (this.useMock);
}
/*******************************************************************************
** Setter for useMock
*******************************************************************************/
public void setUseMock(boolean useMock)
{
this.useMock = useMock;
}
/*******************************************************************************
** Fluent setter for useMock
*******************************************************************************/
public MockApiUtilsHelper withUseMock(boolean useMock)
{
this.useMock = useMock;
return (this);
}
/*******************************************************************************
** Getter for mockResponseQueue
*******************************************************************************/
public Deque<QHttpResponse> getMockResponseQueue()
{
return (this.mockResponseQueue);
}
/*******************************************************************************
** Setter for mockResponseQueue
*******************************************************************************/
public void setMockResponseQueue(Deque<QHttpResponse> mockResponseQueue)
{
this.mockResponseQueue = mockResponseQueue;
}
/*******************************************************************************
** Fluent setter for mockResponseQueue
*******************************************************************************/
public MockApiUtilsHelper withMockResponseQueue(Deque<QHttpResponse> mockResponseQueue)
{
this.mockResponseQueue = mockResponseQueue;
return (this);
}
/*******************************************************************************
** Getter for mockRequestAsserter
*******************************************************************************/
public UnsafeConsumer<HttpRequestBase, ? extends Throwable> getMockRequestAsserter()
{
return (this.mockRequestAsserter);
}
/*******************************************************************************
** Setter for mockRequestAsserter
*******************************************************************************/
public void setMockRequestAsserter(UnsafeConsumer<HttpRequestBase, ? extends Throwable> mockRequestAsserter)
{
this.mockRequestAsserter = mockRequestAsserter;
}
/*******************************************************************************
** Fluent setter for mockRequestAsserter
*******************************************************************************/
public MockApiUtilsHelper withMockRequestAsserter(UnsafeConsumer<HttpRequestBase, ? extends Throwable> mockRequestAsserter)
{
this.mockRequestAsserter = mockRequestAsserter;
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
public QHttpResponse defaultMockMakeRequest(MockApiUtilsHelper mockApiUtilsHelper, QTableMetaData table, HttpRequestBase request, UnsafeSupplier<QHttpResponse, QException> superMethod) throws QException
{
if(!mockApiUtilsHelper.getUseMock())
{
QHttpResponse superResponse = superMethod.get();
System.out.println("== non-mock response content: ==");
System.out.println("Code: " + superResponse.getStatusCode());
System.out.println(superResponse.getContent());
System.out.println("== ==");
return (superResponse);
}
return mockApiUtilsHelper.returnMockResponseFromQueue(request);
}
/*******************************************************************************
**
*******************************************************************************/
@SuppressWarnings("unchecked")
public static String readRequestBody(HttpRequestBase request) throws IOException
{
return (StringUtils.join("\n", IOUtils.readLines(((HttpPost) request).getEntity().getContent())));
}
}

View File

@ -0,0 +1,302 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.module.api.mocks;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.Locale;
import com.kingsrook.qqq.backend.module.api.actions.QHttpResponse;
import org.apache.http.Header;
import org.apache.http.HeaderIterator;
import org.apache.http.HttpEntity;
import org.apache.http.ProtocolVersion;
import org.apache.http.StatusLine;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.entity.BasicHttpEntity;
import org.apache.http.message.BasicStatusLine;
import org.apache.http.params.HttpParams;
/*******************************************************************************
**
*******************************************************************************/
public class MockHttpResponse implements CloseableHttpResponse
{
private final MockApiUtilsHelper mockApiUtilsHelper;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public MockHttpResponse(MockApiUtilsHelper mockApiUtilsHelper)
{
this.mockApiUtilsHelper = mockApiUtilsHelper;
}
@Override
public void close() throws IOException
{
}
@Override
public StatusLine getStatusLine()
{
ProtocolVersion protocolVersion = new ProtocolVersion("HTTP", 1, 1);
if(!mockApiUtilsHelper.getMockResponseQueue().isEmpty())
{
QHttpResponse qHttpResponse = mockApiUtilsHelper.getMockResponseQueue().peekFirst();
return (new BasicStatusLine(protocolVersion, qHttpResponse.getStatusCode(), qHttpResponse.getStatusReasonPhrase()));
}
else
{
return (new BasicStatusLine(protocolVersion, 200, "OK"));
}
}
@Override
public void setStatusLine(StatusLine statusLine)
{
}
@Override
public void setStatusLine(ProtocolVersion protocolVersion, int i)
{
}
@Override
public void setStatusLine(ProtocolVersion protocolVersion, int i, String s)
{
}
@Override
public void setStatusCode(int i) throws IllegalStateException
{
}
@Override
public void setReasonPhrase(String s) throws IllegalStateException
{
}
@Override
public HttpEntity getEntity()
{
BasicHttpEntity basicHttpEntity = new BasicHttpEntity();
if(!mockApiUtilsHelper.getMockResponseQueue().isEmpty())
{
QHttpResponse qHttpResponse = mockApiUtilsHelper.getMockResponseQueue().removeFirst();
basicHttpEntity.setContent(new ByteArrayInputStream(qHttpResponse.getContent().getBytes()));
}
else
{
basicHttpEntity.setContent(new ByteArrayInputStream("".getBytes()));
}
return (basicHttpEntity);
}
@Override
public void setEntity(HttpEntity httpEntity)
{
}
@Override
public Locale getLocale()
{
return null;
}
@Override
public void setLocale(Locale locale)
{
}
@Override
public ProtocolVersion getProtocolVersion()
{
return null;
}
@Override
public boolean containsHeader(String s)
{
return false;
}
@Override
public Header[] getHeaders(String s)
{
return new Header[0];
}
@Override
public Header getFirstHeader(String s)
{
return null;
}
@Override
public Header getLastHeader(String s)
{
return null;
}
@Override
public Header[] getAllHeaders()
{
return new Header[0];
}
@Override
public void addHeader(Header header)
{
}
@Override
public void addHeader(String s, String s1)
{
}
@Override
public void setHeader(Header header)
{
}
@Override
public void setHeader(String s, String s1)
{
}
@Override
public void setHeaders(Header[] headers)
{
}
@Override
public void removeHeader(Header header)
{
}
@Override
public void removeHeaders(String s)
{
}
@Override
public HeaderIterator headerIterator()
{
return null;
}
@Override
public HeaderIterator headerIterator(String s)
{
return null;
}
@Override
public HttpParams getParams()
{
return null;
}
@Override
public void setParams(HttpParams httpParams)
{
}
}

View File

@ -50,6 +50,7 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
@ -87,7 +88,7 @@ public class QueryManager
/*******************************************************************************
**
*******************************************************************************/
void processResultSet(ResultSet rs) throws SQLException;
void processResultSet(ResultSet rs) throws SQLException, QException;
}
@ -95,7 +96,7 @@ public class QueryManager
/*******************************************************************************
**
*******************************************************************************/
public static void executeStatement(Connection connection, String sql, ResultSetProcessor processor, Object... params) throws SQLException
public static void executeStatement(Connection connection, String sql, ResultSetProcessor processor, Object... params) throws SQLException, QException
{
PreparedStatement statement = null;
try
@ -118,7 +119,7 @@ public class QueryManager
** Let the caller provide their own prepared statement (e.g., possibly with some
** customized settings/optimizations).
*******************************************************************************/
public static void executeStatement(PreparedStatement statement, ResultSetProcessor processor, Object... params) throws SQLException
public static void executeStatement(PreparedStatement statement, ResultSetProcessor processor, Object... params) throws SQLException, QException
{
ResultSet resultSet = null;

View File

@ -33,10 +33,8 @@
<properties>
<!-- props specifically to this module -->
<!-- none at this time -->
<!-- todo - remove these!! -->
<coverage.instructionCoveredRatioMinimum>0.10</coverage.instructionCoveredRatioMinimum>
<coverage.classCoveredRatioMinimum>0.10</coverage.classCoveredRatioMinimum>
</properties>
<dependencies>

View File

@ -64,6 +64,11 @@ public class QRecordApiAdapter
*******************************************************************************/
public static Map<String, Serializable> qRecordToApiMap(QRecord record, String tableName, String apiName, String apiVersion) throws QException
{
if(record == null)
{
return (null);
}
List<QFieldMetaData> tableApiFields = getTableApiFieldList(new ApiNameVersionAndTableName(apiName, apiVersion, tableName));
LinkedHashMap<String, Serializable> outputRecord = new LinkedHashMap<>();

View File

@ -26,7 +26,7 @@ import java.io.Serializable;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
import com.kingsrook.qqq.api.model.metadata.APILogMetaDataProvider;
import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaDataProvider;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QRuntimeException;
import com.kingsrook.qqq.backend.core.model.data.QField;
@ -49,7 +49,7 @@ public class APILog extends QRecordEntity
@QField(isEditable = false)
private Instant timestamp;
@QField(possibleValueSourceName = APILogMetaDataProvider.TABLE_NAME_API_LOG_USER, label = "User")
@QField(possibleValueSourceName = ApiInstanceMetaDataProvider.TABLE_NAME_API_LOG_USER, label = "User")
private Integer apiLogUserId;
@QField(possibleValueSourceName = "apiMethod")

View File

@ -50,7 +50,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
/*******************************************************************************
**
*******************************************************************************/
public class APILogMetaDataProvider
public class ApiInstanceMetaDataProvider
{
public static final String TABLE_NAME_API_LOG = "apiLog";
public static final String TABLE_NAME_API_LOG_USER = "apiLogUser";
@ -105,21 +105,32 @@ public class APILogMetaDataProvider
new QPossibleValue<>(500, "500 (Internal Server Error)")
)));
////////////////////////////////////////////////////////////////////////////
// loop over api names and versions, building out possible values sources //
////////////////////////////////////////////////////////////////////////////
List<QPossibleValue<?>> apiNamePossibleValues = new ArrayList<>();
List<QPossibleValue<?>> apiVersionPossibleValues = new ArrayList<>();
////////////////////////////////////////////////////////////////////////////////////////////////////
// todo... this, this whole thing, should probably have "which api" as another field too... ugh. //
////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////
// todo... apiName should maybe be a field on apiLog table, eh? //
//////////////////////////////////////////////////////////////////
TreeSet<APIVersion> allVersions = new TreeSet<>();
ApiInstanceMetaDataContainer apiInstanceMetaDataContainer = ApiInstanceMetaDataContainer.of(instance);
for(Map.Entry<String, ApiInstanceMetaData> entry : apiInstanceMetaDataContainer.getApis().entrySet())
{
apiNamePossibleValues.add(new QPossibleValue<>(entry.getKey(), entry.getValue().getLabel()));
ApiInstanceMetaData apiInstanceMetaData = entry.getValue();
allVersions.addAll(apiInstanceMetaData.getPastVersions());
allVersions.addAll(apiInstanceMetaData.getSupportedVersions());
allVersions.addAll(apiInstanceMetaData.getFutureVersions());
}
instance.addPossibleValueSource(new QPossibleValueSource()
.withName("apiName")
.withType(QPossibleValueSourceType.ENUM)
.withEnumValues(apiNamePossibleValues));
for(APIVersion version : allVersions)
{
apiVersionPossibleValues.add(new QPossibleValue<>(version.toString()));

View File

@ -0,0 +1,260 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.api.utils;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.api.actions.ApiImplementation;
import com.kingsrook.qqq.api.actions.QRecordApiAdapter;
import com.kingsrook.qqq.api.model.APIVersion;
import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaData;
import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaDataContainer;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
/*******************************************************************************
** Object injected into script context, for interfacing with a QQQ API.
*******************************************************************************/
public class ApiScriptUtils implements Serializable
{
private String apiName;
private String apiVersion;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public ApiScriptUtils(String apiName, String apiVersion)
{
setApiName(apiName);
setApiVersion(apiVersion);
}
/*******************************************************************************
**
*******************************************************************************/
public static ArrayList<Map<String, Serializable>> qRecordListToApiRecordList(List<QRecord> qRecordList, String tableName, String apiName, String apiVersion) throws QException
{
if(qRecordList == null)
{
return (null);
}
ArrayList<Map<String, Serializable>> rs = new ArrayList<>();
for(QRecord qRecord : qRecordList)
{
rs.add(QRecordApiAdapter.qRecordToApiMap(qRecord, tableName, apiName, apiVersion));
}
return (rs);
}
/*******************************************************************************
** Setter for apiName
**
*******************************************************************************/
public void setApiName(String apiName)
{
ApiInstanceMetaDataContainer apiInstanceMetaDataContainer = ApiInstanceMetaDataContainer.of(QContext.getQInstance());
if(apiInstanceMetaDataContainer.getApis().containsKey(apiName))
{
this.apiName = apiName;
}
else
{
throw (new IllegalArgumentException("[" + apiName + "] is not a valid API name. Valid values are: " + apiInstanceMetaDataContainer.getApis().keySet()));
}
}
/*******************************************************************************
** Setter for apiVersion
**
*******************************************************************************/
public void setApiVersion(String apiVersion)
{
if(apiName == null)
{
throw (new IllegalArgumentException("You must set apiName before setting apiVersion."));
}
ApiInstanceMetaDataContainer apiInstanceMetaDataContainer = ApiInstanceMetaDataContainer.of(QContext.getQInstance());
ApiInstanceMetaData apiInstanceMetaData = apiInstanceMetaDataContainer.getApis().get(apiName);
if(apiInstanceMetaData.getSupportedVersions().contains(new APIVersion(apiVersion)))
{
this.apiVersion = apiVersion;
}
else
{
throw (new IllegalArgumentException("[" + apiVersion + "] is not a supported version for this API. Supported versions are: " + apiInstanceMetaData.getSupportedVersions()));
}
}
/*******************************************************************************
**
*******************************************************************************/
private void validateApiNameAndVersion(String description)
{
if(apiName == null || apiVersion == null)
{
throw (new IllegalStateException("Both apiName and apiVersion must be set before calling this method (" + description + ")."));
}
}
/*******************************************************************************
**
*******************************************************************************/
public Map<String, Serializable> get(String tableApiName, Object primaryKey) throws QException
{
validateApiNameAndVersion("get(" + tableApiName + "," + primaryKey + ")");
return (ApiImplementation.get(getApiInstanceMetaData(), apiVersion, tableApiName, String.valueOf(primaryKey)));
}
/*******************************************************************************
**
*******************************************************************************/
public Map<String, Serializable> query(String tableName, String queryString) throws QException
{
validateApiNameAndVersion("query(" + tableName + ")");
Map<String, List<String>> paramMap = parseQueryString(queryString);
return (ApiImplementation.query(getApiInstanceMetaData(), apiVersion, tableName, paramMap));
}
/*******************************************************************************
**
*******************************************************************************/
public Map<String, Serializable> insert(String tableApiName, Object body) throws QException
{
validateApiNameAndVersion("insert(" + tableApiName + ")");
return (ApiImplementation.insert(getApiInstanceMetaData(), apiVersion, tableApiName, String.valueOf(body)));
}
/*******************************************************************************
**
*******************************************************************************/
public List<Map<String, Serializable>> bulkInsert(String tableApiName, Object body) throws QException
{
validateApiNameAndVersion("bulkInsert(" + tableApiName + ")");
return (ApiImplementation.bulkInsert(getApiInstanceMetaData(), apiVersion, tableApiName, String.valueOf(body)));
}
/*******************************************************************************
**
*******************************************************************************/
public void update(String tableApiName, Object primaryKey, Object body) throws QException
{
validateApiNameAndVersion("update(" + tableApiName + "," + primaryKey + ")");
ApiImplementation.update(getApiInstanceMetaData(), apiVersion, tableApiName, String.valueOf(primaryKey), String.valueOf(body));
}
/*******************************************************************************
**
*******************************************************************************/
public List<Map<String, Serializable>> bulkUpdate(String tableApiName, Object body) throws QException
{
validateApiNameAndVersion("bulkUpdate(" + tableApiName + ")");
return (ApiImplementation.bulkUpdate(getApiInstanceMetaData(), apiVersion, tableApiName, String.valueOf(body)));
}
/*******************************************************************************
**
*******************************************************************************/
public void delete(String tableApiName, Object primaryKey) throws QException
{
validateApiNameAndVersion("delete(" + tableApiName + "," + primaryKey + ")");
ApiImplementation.delete(getApiInstanceMetaData(), apiVersion, tableApiName, String.valueOf(primaryKey));
}
/*******************************************************************************
**
*******************************************************************************/
public List<Map<String, Serializable>> bulkDelete(String tableApiName, Object body) throws QException
{
validateApiNameAndVersion("bulkDelete(" + tableApiName + ")");
return (ApiImplementation.bulkDelete(getApiInstanceMetaData(), apiVersion, tableApiName, String.valueOf(body)));
}
/*******************************************************************************
**
*******************************************************************************/
private ApiInstanceMetaData getApiInstanceMetaData()
{
ApiInstanceMetaDataContainer apiInstanceMetaDataContainer = ApiInstanceMetaDataContainer.of(QContext.getQInstance());
ApiInstanceMetaData apiInstanceMetaData = apiInstanceMetaDataContainer.getApiInstanceMetaData(apiName);
return apiInstanceMetaData;
}
/*******************************************************************************
**
*******************************************************************************/
private static Map<String, List<String>> parseQueryString(String queryString)
{
Map<String, List<String>> paramMap = new LinkedHashMap<>();
if(queryString != null)
{
for(String nameValuePair : queryString.split("&"))
{
String[] nameValue = nameValuePair.split("=", 2);
if(nameValue.length == 2)
{
paramMap.computeIfAbsent(nameValue[0], (k) -> new ArrayList<>());
paramMap.get(nameValue[0]).add(nameValue[1]);
}
}
}
return paramMap;
}
}

View File

@ -22,6 +22,7 @@
package com.kingsrook.qqq.api;
import java.time.LocalDate;
import java.util.List;
import com.kingsrook.qqq.api.model.APIVersion;
import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaData;
@ -30,7 +31,11 @@ import com.kingsrook.qqq.api.model.metadata.fields.ApiFieldMetaData;
import com.kingsrook.qqq.api.model.metadata.fields.ApiFieldMetaDataContainer;
import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaData;
import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaDataContainer;
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
@ -324,4 +329,40 @@ public class TestUtils
.withOrderBy(new QFilterOrderBy("key"));
}
/*******************************************************************************
**
*******************************************************************************/
public static void insertPersonRecord(Integer id, String firstName, String lastName) throws QException
{
insertPersonRecord(id, firstName, lastName, null);
}
/*******************************************************************************
**
*******************************************************************************/
public static void insertPersonRecord(Integer id, String firstName, String lastName, LocalDate birthDate) throws QException
{
InsertInput insertInput = new InsertInput();
insertInput.setTableName(TestUtils.TABLE_NAME_PERSON);
insertInput.setRecords(List.of(new QRecord().withValue("id", id).withValue("firstName", firstName).withValue("lastName", lastName).withValue("birthDate", birthDate)));
new InsertAction().execute(insertInput);
}
/*******************************************************************************
**
*******************************************************************************/
public static void insertSimpsons() throws QException
{
insertPersonRecord(1, "Homer", "Simpson");
insertPersonRecord(2, "Marge", "Simpson");
insertPersonRecord(3, "Bart", "Simpson");
insertPersonRecord(4, "Lisa", "Simpson");
insertPersonRecord(5, "Maggie", "Simpson");
}
}

View File

@ -61,6 +61,8 @@ import org.json.JSONObject;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import static com.kingsrook.qqq.api.TestUtils.insertPersonRecord;
import static com.kingsrook.qqq.api.TestUtils.insertSimpsons;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
@ -145,25 +147,29 @@ class QJavalinApiHandlerTest extends BaseTest
@Test
void testRandom404s()
{
for(String method : new String[] { "get", "post", "patch", "delete" })
{
assertErrorResponse(HttpStatus.NOT_FOUND_404, "Could not find any resources at path", Unirest.request(method, BASE_URL + "/api/" + VERSION + "/notATable/").asString());
assertErrorResponse(HttpStatus.NOT_FOUND_404, "Could not find any resources at path", Unirest.request(method, BASE_URL + "/api/" + VERSION + "/notATable/notAnId").asString());
assertErrorResponse(HttpStatus.NOT_FOUND_404, "Could not find any resources at path", Unirest.request(method, BASE_URL + "/api/" + VERSION + "/person/1/2").asString());
assertErrorResponse(HttpStatus.NOT_FOUND_404, "Could not find any resources at path", Unirest.request(method, BASE_URL + "/api/foo").asString());
assertErrorResponse(HttpStatus.NOT_FOUND_404, "Could not find any resources at path", Unirest.request("get", BASE_URL + "/api/" + VERSION + "/notATable/").asString());
assertErrorResponse(HttpStatus.NOT_FOUND_404, "Could not find a table named notATable in this api", Unirest.request("get", BASE_URL + "/api/" + VERSION + "/notATable/notAnId").asString());
assertErrorResponse(HttpStatus.NOT_FOUND_404, "Could not find any resources at path", Unirest.request("get", BASE_URL + "/api/" + VERSION + "/person/1/2").asString());
assertErrorResponse(HttpStatus.NOT_FOUND_404, "Could not find any resources at path", Unirest.request("get", BASE_URL + "/api/foo").asString());
assertErrorResponse(HttpStatus.OK_200, null, Unirest.request("get", BASE_URL + "/api/").asString()); // this path returns the doc site for a GET
if(method.equals("get"))
{
//////////////////////////////////////////////
// this path returns the doc site for a GET //
//////////////////////////////////////////////
assertErrorResponse(HttpStatus.OK_200, null, Unirest.request(method, BASE_URL + "/api/").asString());
}
else
{
assertErrorResponse(HttpStatus.NOT_FOUND_404, "Could not find any resources at path", Unirest.request(method, BASE_URL + "/api/").asString());
}
}
assertErrorResponse(HttpStatus.NOT_FOUND_404, "Could not find a table named notATable in this api", Unirest.request("post", BASE_URL + "/api/" + VERSION + "/notATable/").asString());
assertErrorResponse(HttpStatus.NOT_FOUND_404, "Could not find any resources at path", Unirest.request("post", BASE_URL + "/api/" + VERSION + "/notATable/notAnId").asString());
assertErrorResponse(HttpStatus.NOT_FOUND_404, "Could not find any resources at path", Unirest.request("post", BASE_URL + "/api/" + VERSION + "/person/1/2").asString());
assertErrorResponse(HttpStatus.NOT_FOUND_404, "Could not find any resources at path", Unirest.request("post", BASE_URL + "/api/foo").asString());
assertErrorResponse(HttpStatus.NOT_FOUND_404, "Could not find any resources at path", Unirest.request("post", BASE_URL + "/api/").asString());
assertErrorResponse(HttpStatus.NOT_FOUND_404, "Could not find any resources at path", Unirest.request("patch", BASE_URL + "/api/" + VERSION + "/notATable/").asString());
assertErrorResponse(HttpStatus.NOT_FOUND_404, "Could not find a table named notATable in this api", Unirest.request("patch", BASE_URL + "/api/" + VERSION + "/notATable/notAnId").asString());
assertErrorResponse(HttpStatus.NOT_FOUND_404, "Could not find any resources at path", Unirest.request("patch", BASE_URL + "/api/" + VERSION + "/person/1/2").asString());
assertErrorResponse(HttpStatus.NOT_FOUND_404, "Could not find any resources at path", Unirest.request("patch", BASE_URL + "/api/foo").asString());
assertErrorResponse(HttpStatus.NOT_FOUND_404, "Could not find any resources at path", Unirest.request("patch", BASE_URL + "/api/").asString());
assertErrorResponse(HttpStatus.NOT_FOUND_404, "Could not find any resources at path", Unirest.request("delete", BASE_URL + "/api/" + VERSION + "/notATable/").asString());
assertErrorResponse(HttpStatus.NOT_FOUND_404, "Could not find a table named notATable in this api", Unirest.request("delete", BASE_URL + "/api/" + VERSION + "/notATable/notAnId").asString());
assertErrorResponse(HttpStatus.NOT_FOUND_404, "Could not find any resources at path", Unirest.request("delete", BASE_URL + "/api/" + VERSION + "/person/1/2").asString());
assertErrorResponse(HttpStatus.NOT_FOUND_404, "Could not find any resources at path", Unirest.request("delete", BASE_URL + "/api/foo").asString());
assertErrorResponse(HttpStatus.NOT_FOUND_404, "Could not find any resources at path", Unirest.request("delete", BASE_URL + "/api/").asString());
}
@ -1240,43 +1246,6 @@ class QJavalinApiHandlerTest extends BaseTest
/*******************************************************************************
**
*******************************************************************************/
private static void insertPersonRecord(Integer id, String firstName, String lastName) throws QException
{
insertPersonRecord(id, firstName, lastName, null);
}
/*******************************************************************************
**
*******************************************************************************/
private static void insertPersonRecord(Integer id, String firstName, String lastName, LocalDate birthDate) throws QException
{
InsertInput insertInput = new InsertInput();
insertInput.setTableName(TestUtils.TABLE_NAME_PERSON);
insertInput.setRecords(List.of(new QRecord().withValue("id", id).withValue("firstName", firstName).withValue("lastName", lastName).withValue("birthDate", birthDate)));
new InsertAction().execute(insertInput);
}
/*******************************************************************************
**
*******************************************************************************/
private static void insertSimpsons() throws QException
{
insertPersonRecord(1, "Homer", "Simpson");
insertPersonRecord(2, "Marge", "Simpson");
insertPersonRecord(3, "Bart", "Simpson");
insertPersonRecord(4, "Lisa", "Simpson");
insertPersonRecord(5, "Maggie", "Simpson");
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -0,0 +1,273 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.api.utils;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.api.BaseTest;
import com.kingsrook.qqq.api.TestUtils;
import com.kingsrook.qqq.api.javalin.QBadRequestException;
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.tables.get.GetInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import org.junit.jupiter.api.Test;
import static com.kingsrook.qqq.api.TestUtils.insertSimpsons;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
/*******************************************************************************
** Unit test for com.kingsrook.qqq.api.utils.ApiScriptUtils
*******************************************************************************/
class ApiScriptUtilsTest extends BaseTest
{
/*******************************************************************************
**
*******************************************************************************/
@Test
void testSetApiNameAndApiVersion()
{
ApiScriptUtils apiScriptUtils = newDefaultApiScriptUtils();
assertThatThrownBy(() -> apiScriptUtils.setApiName("not an api"))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("not a valid API name");
assertThatThrownBy(() -> apiScriptUtils.setApiVersion("not a version"))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("not a supported version");
assertThatThrownBy(() -> new ApiScriptUtils("not an api", TestUtils.CURRENT_API_VERSION))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("not a valid API name");
assertThatThrownBy(() -> new ApiScriptUtils(TestUtils.ALTERNATIVE_API_NAME, "not a version"))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("not a supported version");
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testGet() throws QException
{
ApiScriptUtils apiScriptUtils = newDefaultApiScriptUtils();
assertThatThrownBy(() -> apiScriptUtils.get(TestUtils.TABLE_NAME_PERSON, 1))
.isInstanceOf(QNotFoundException.class);
insertSimpsons();
Map<String, Serializable> result = apiScriptUtils.get(TestUtils.TABLE_NAME_PERSON, 1);
assertEquals("Homer", result.get("firstName"));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testQuery() throws QException
{
ApiScriptUtils apiScriptUtils = newDefaultApiScriptUtils();
assertThatThrownBy(() -> apiScriptUtils.query(TestUtils.TABLE_NAME_PERSON, "foo=bar"))
.isInstanceOf(QBadRequestException.class)
.hasMessageContaining("Unrecognized filter criteria field: foo");
insertSimpsons();
Map<String, Serializable> result = apiScriptUtils.query(TestUtils.TABLE_NAME_PERSON, "id=2");
assertEquals(1, result.get("count"));
assertEquals(1, ((List<?>) result.get("records")).size());
assertEquals("Marge", ((Map<?, ?>) ((List<?>) result.get("records")).get(0)).get("firstName"));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testInsert() throws QException
{
ApiScriptUtils apiScriptUtils = newDefaultApiScriptUtils();
Map<String, Serializable> result = apiScriptUtils.insert(TestUtils.TABLE_NAME_PERSON, """
{ "firstName": "Mr.", "lastName": "Burns" }
""");
assertEquals(1, result.get("id"));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testBulkInsert() throws QException
{
ApiScriptUtils apiScriptUtils = newDefaultApiScriptUtils();
List<Map<String, Serializable>> result = apiScriptUtils.bulkInsert(TestUtils.TABLE_NAME_PERSON, """
[
{ "firstName": "Mr.", "lastName": "Burns" },
{ "firstName": "Waylon", "lastName": "Smithers" }
]
""");
assertEquals(2, result.size());
assertEquals(1, result.get(0).get("id"));
assertEquals(2, result.get(1).get("id"));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testUpdate() throws QException
{
ApiScriptUtils apiScriptUtils = newDefaultApiScriptUtils();
String updateJSON = """
{ "firstName": "Homer J." }
""";
assertThatThrownBy(() -> apiScriptUtils.update(TestUtils.TABLE_NAME_PERSON, 1, updateJSON))
.isInstanceOf(QNotFoundException.class);
insertSimpsons();
apiScriptUtils.update(TestUtils.TABLE_NAME_PERSON, 1, updateJSON);
GetInput getInput = new GetInput();
getInput.setTableName(TestUtils.TABLE_NAME_PERSON);
getInput.setPrimaryKey(1);
GetOutput getOutput = new GetAction().execute(getInput);
assertEquals("Homer J.", getOutput.getRecord().getValueString("firstName"));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testBulkUpdate() throws QException
{
ApiScriptUtils apiScriptUtils = newDefaultApiScriptUtils();
insertSimpsons();
String updateJSON = """
[
{ "id": 1, "firstName": "Homer J." },
{ "id": 6, "firstName": "C.M." }
]
""";
List<Map<String, Serializable>> result = apiScriptUtils.bulkUpdate(TestUtils.TABLE_NAME_PERSON, updateJSON);
assertEquals(2, result.size());
assertEquals(1, result.get(0).get("id"));
assertEquals(6, result.get(1).get("id"));
assertEquals(404, result.get(1).get("statusCode"));
assertNotNull(result.get(1).get("error"));
GetInput getInput = new GetInput();
getInput.setTableName(TestUtils.TABLE_NAME_PERSON);
getInput.setPrimaryKey(1);
GetOutput getOutput = new GetAction().execute(getInput);
assertEquals("Homer J.", getOutput.getRecord().getValueString("firstName"));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testDelete() throws QException
{
ApiScriptUtils apiScriptUtils = newDefaultApiScriptUtils();
assertThatThrownBy(() -> apiScriptUtils.delete(TestUtils.TABLE_NAME_PERSON, 1))
.isInstanceOf(QNotFoundException.class);
insertSimpsons();
apiScriptUtils.delete(TestUtils.TABLE_NAME_PERSON, 1);
GetInput getInput = new GetInput();
getInput.setTableName(TestUtils.TABLE_NAME_PERSON);
getInput.setPrimaryKey(1);
GetOutput getOutput = new GetAction().execute(getInput);
assertNull(getOutput.getRecord());
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testBulkDelete() throws QException
{
ApiScriptUtils apiScriptUtils = newDefaultApiScriptUtils();
insertSimpsons();
List<Map<String, Serializable>> result = apiScriptUtils.bulkDelete(TestUtils.TABLE_NAME_PERSON, "[1,6]");
assertEquals(2, result.size());
assertEquals(1, ValueUtils.getValueAsInteger(result.get(0).get("id")));
assertEquals(6, ValueUtils.getValueAsInteger(result.get(1).get("id")));
assertEquals(404, result.get(1).get("statusCode"));
assertNotNull(result.get(1).get("error"));
GetInput getInput = new GetInput();
getInput.setTableName(TestUtils.TABLE_NAME_PERSON);
getInput.setPrimaryKey(1);
GetOutput getOutput = new GetAction().execute(getInput);
assertNull(getOutput.getRecord());
}
/*******************************************************************************
**
*******************************************************************************/
private static ApiScriptUtils newDefaultApiScriptUtils()
{
return (new ApiScriptUtils(TestUtils.API_NAME, TestUtils.CURRENT_API_VERSION));
}
}

View File

@ -395,18 +395,18 @@ public class QJavalinScriptsHandler
String key = entry.getKey();
String value = entry.getValue().get(0);
if(key.equals("code"))
switch(key)
{
input.setCodeReference(new QCodeReference().withInlineCode(value).withCodeType(QCodeType.JAVA_SCRIPT));
}
else
{
inputValues.put(key, value);
case "code" -> input.setCodeReference(new QCodeReference().withInlineCode(value).withCodeType(QCodeType.JAVA_SCRIPT));
case "apiName" -> input.setApiName(value);
case "apiVersion" -> input.setApiVersion(value);
default -> inputValues.put(key, value);
}
}
TestScriptActionInterface scriptTester = QCodeLoader.getAdHoc(TestScriptActionInterface.class, scriptTesterCodeRef);
TestScriptOutput output = new TestScriptOutput();
scriptTester.execute(input, output);
QJavalinAccessLogger.logEndSuccess();