Merged feature/qol-improvements-20240801 into dev

This commit is contained in:
2024-08-05 13:36:24 -05:00
17 changed files with 492 additions and 79 deletions

View File

@ -37,7 +37,7 @@ import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
** **
** Note: One would imagine that this class shouldn't ever implement Serializable... ** Note: One would imagine that this class shouldn't ever implement Serializable...
*******************************************************************************/ *******************************************************************************/
public class QBackendTransaction public class QBackendTransaction implements AutoCloseable
{ {
/******************************************************************************* /*******************************************************************************

View File

@ -227,6 +227,11 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
*******************************************************************************/ *******************************************************************************/
public void performValidations(InsertInput insertInput, boolean isPreview) throws QException public void performValidations(InsertInput insertInput, boolean isPreview) throws QException
{ {
if(CollectionUtils.nullSafeIsEmpty(insertInput.getRecords()))
{
return;
}
QTableMetaData table = insertInput.getTable(); QTableMetaData table = insertInput.getTable();
/////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////

View File

@ -68,7 +68,7 @@ public class QValueFormatter
*******************************************************************************/ *******************************************************************************/
public static String formatValue(QFieldMetaData field, Serializable value) public static String formatValue(QFieldMetaData field, Serializable value)
{ {
return (formatValue(field.getDisplayFormat(), field.getName(), value)); return (formatValue(field.getDisplayFormat(), field.getType(), field.getName(), value));
} }
@ -78,7 +78,7 @@ public class QValueFormatter
*******************************************************************************/ *******************************************************************************/
public static String formatValue(String displayFormat, Serializable value) public static String formatValue(String displayFormat, Serializable value)
{ {
return (formatValue(displayFormat, "", value)); return (formatValue(displayFormat, null, "", value));
} }
@ -87,7 +87,7 @@ public class QValueFormatter
** For a display format string, an optional fieldName (only used for logging), ** For a display format string, an optional fieldName (only used for logging),
** and a value, apply the format. ** and a value, apply the format.
*******************************************************************************/ *******************************************************************************/
private static String formatValue(String displayFormat, String fieldName, Serializable value) private static String formatValue(String displayFormat, QFieldType fieldType, String fieldName, Serializable value)
{ {
////////////////////////////////// //////////////////////////////////
// null values get null results // // null values get null results //
@ -107,6 +107,11 @@ public class QValueFormatter
return formatBoolean(b); return formatBoolean(b);
} }
if(QFieldType.BOOLEAN.equals(fieldType))
{
return formatBoolean(ValueUtils.getValueAsBoolean(value));
}
if(value instanceof LocalTime lt) if(value instanceof LocalTime lt)
{ {
return formatLocalTime(lt); return formatLocalTime(lt);
@ -404,6 +409,7 @@ public class QValueFormatter
} }
/******************************************************************************* /*******************************************************************************
** For a single record, set its display values - where caller (meant to stay private) ** For a single record, set its display values - where caller (meant to stay private)
** can specify if they've already done fieldBehaviors (to avoid re-doing). ** can specify if they've already done fieldBehaviors (to avoid re-doing).

View File

@ -22,8 +22,8 @@
package com.kingsrook.qqq.backend.core.exceptions; package com.kingsrook.qqq.backend.core.exceptions;
import java.util.Arrays;
import java.util.List; import java.util.List;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils;
@ -55,12 +55,11 @@ public class QInstanceValidationException extends QException
*******************************************************************************/ *******************************************************************************/
public QInstanceValidationException(List<String> reasons) public QInstanceValidationException(List<String> reasons)
{ {
super( super((CollectionUtils.nullSafeHasContents(reasons))
(reasons != null && reasons.size() > 0) ? "Instance validation failed for the following reasons:\n - " + StringUtils.join("\n - ", reasons) + "\n(" + reasons.size() + " Total reason" + StringUtils.plural(reasons) + ")"
? "Instance validation failed for the following reasons:\n - " + StringUtils.join("\n - ", reasons) : "Validation failed, but no reasons were provided");
: "Validation failed, but no reasons were provided");
if(reasons != null && reasons.size() > 0) if(CollectionUtils.nullSafeHasContents(reasons))
{ {
this.reasons = reasons; this.reasons = reasons;
} }
@ -68,25 +67,6 @@ public class QInstanceValidationException extends QException
/*******************************************************************************
** Constructor of an array/varargs of reasons. They feed into the core exception message.
**
*******************************************************************************/
public QInstanceValidationException(String... reasons)
{
super(
(reasons != null && reasons.length > 0)
? "Instance validation failed for the following reasons: " + StringUtils.joinWithCommasAndAnd(Arrays.stream(reasons).toList())
: "Validation failed, but no reasons were provided");
if(reasons != null && reasons.length > 0)
{
this.reasons = Arrays.stream(reasons).toList();
}
}
/******************************************************************************* /*******************************************************************************
** Constructor of message & cause - does not populate reasons! ** Constructor of message & cause - does not populate reasons!
** **

View File

@ -59,6 +59,30 @@ public class AggregateInput extends AbstractTableActionInput
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public AggregateInput(String tableName)
{
this();
setTableName(tableName);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public AggregateInput withTableName(String tableName)
{
super.withTableName(tableName);
return (this);
}
/******************************************************************************* /*******************************************************************************
** Getter for filter ** Getter for filter
** **

View File

@ -29,6 +29,7 @@ import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
import com.kingsrook.qqq.backend.core.actions.tables.GetAction; import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QNotFoundException;
import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput; import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput; import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
@ -44,6 +45,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.savedviews.SavedView; import com.kingsrook.qqq.backend.core.model.savedviews.SavedView;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/******************************************************************************* /*******************************************************************************
@ -78,10 +80,10 @@ public class QuerySavedViewProcess implements BackendStep
public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
{ {
ActionHelper.validateSession(runBackendStepInput); ActionHelper.validateSession(runBackendStepInput);
Integer savedViewId = runBackendStepInput.getValueInteger("id");
try try
{ {
Integer savedViewId = runBackendStepInput.getValueInteger("id");
if(savedViewId != null) if(savedViewId != null)
{ {
GetInput input = new GetInput(); GetInput input = new GetInput();
@ -89,6 +91,11 @@ public class QuerySavedViewProcess implements BackendStep
input.setPrimaryKey(savedViewId); input.setPrimaryKey(savedViewId);
GetOutput output = new GetAction().execute(input); GetOutput output = new GetAction().execute(input);
if(output.getRecord() == null)
{
throw (new QNotFoundException("The requested view was not found."));
}
runBackendStepOutput.addRecord(output.getRecord()); runBackendStepOutput.addRecord(output.getRecord());
runBackendStepOutput.addValue("savedView", output.getRecord()); runBackendStepOutput.addValue("savedView", output.getRecord());
runBackendStepOutput.addValue("savedViewList", (Serializable) List.of(output.getRecord())); runBackendStepOutput.addValue("savedViewList", (Serializable) List.of(output.getRecord()));
@ -108,6 +115,11 @@ public class QuerySavedViewProcess implements BackendStep
runBackendStepOutput.addValue("savedViewList", (Serializable) output.getRecords()); runBackendStepOutput.addValue("savedViewList", (Serializable) output.getRecords());
} }
} }
catch(QNotFoundException qnfe)
{
LOG.info("View not found", logPair("savedViewId", savedViewId));
throw (qnfe);
}
catch(Exception e) catch(Exception e)
{ {
LOG.warn("Error querying for saved views", e); LOG.warn("Error querying for saved views", e);

View File

@ -433,6 +433,8 @@ public class ProcessLockUtils
{ {
throw (new QException("Error deleting processLock record: " + deleteOutput.getRecordsWithErrors().get(0).getErrorsAsString())); throw (new QException("Error deleting processLock record: " + deleteOutput.getRecordsWithErrors().get(0).getErrorsAsString()));
} }
LOG.info("Released process lock", logPair("id", processLock.getId()), logPair("key", processLock.getKey()), logPair("typeId", processLock.getProcessLockTypeId()), logPair("details", processLock.getDetails()));
} }
catch(QException e) catch(QException e)
{ {

View File

@ -0,0 +1,126 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. 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.utils;
import java.io.Serializable;
import java.util.AbstractMap;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import com.kingsrook.qqq.backend.core.utils.collections.MutableMap;
/*******************************************************************************
** Hash that provides "counting" capability -- keys map to Integers that
** are automatically/easily summed to
**
*******************************************************************************/
public class CountingHash<K extends Serializable> extends AbstractMap<K, Integer> implements Serializable
{
private Map<K, Integer> map = null;
/*******************************************************************************
** Default constructor
**
*******************************************************************************/
public CountingHash()
{
this.map = new HashMap<>();
}
/*******************************************************************************
** Constructor where you can supply a source map (e.g., if you want a specific
** Map type (like LinkedHashMap), or with pre-values.
**
** Note - the input map will be wrapped in a MutableMap - so - it'll be mutable.
**
*******************************************************************************/
public CountingHash(Map<K, Integer> sourceMap)
{
this.map = new MutableMap<>(sourceMap);
}
/*******************************************************************************
** Increment the value for the specified key by 1.
**
*******************************************************************************/
public Integer add(K key)
{
Integer value = getOrCreateListForKey(key);
Integer sum = value + 1;
map.put(key, sum);
return (sum);
}
/*******************************************************************************
** Increment the value for the specified key by the supplied addend
**
*******************************************************************************/
public Integer add(K key, Integer addend)
{
Integer value = getOrCreateListForKey(key);
Integer sum = value + addend;
map.put(key, sum);
return (sum);
}
/*******************************************************************************
**
*******************************************************************************/
private Integer getOrCreateListForKey(K key)
{
Integer value;
if(!this.map.containsKey(key))
{
this.map.put(key, 0);
value = 0;
}
else
{
value = this.map.get(key);
}
return value;
}
/***************************************************************************
*
***************************************************************************/
public Set<Entry<K, Integer>> entrySet()
{
return this.map.entrySet();
}
}

View File

@ -89,6 +89,10 @@ class QValueFormatterTest extends BaseTest
assertNull(QValueFormatter.formatValue(new QFieldMetaData().withType(QFieldType.BOOLEAN), null)); assertNull(QValueFormatter.formatValue(new QFieldMetaData().withType(QFieldType.BOOLEAN), null));
assertEquals("Yes", QValueFormatter.formatValue(new QFieldMetaData().withType(QFieldType.BOOLEAN), true)); assertEquals("Yes", QValueFormatter.formatValue(new QFieldMetaData().withType(QFieldType.BOOLEAN), true));
assertEquals("No", QValueFormatter.formatValue(new QFieldMetaData().withType(QFieldType.BOOLEAN), false)); assertEquals("No", QValueFormatter.formatValue(new QFieldMetaData().withType(QFieldType.BOOLEAN), false));
assertEquals("Yes", QValueFormatter.formatValue(new QFieldMetaData().withType(QFieldType.BOOLEAN), "true"));
assertEquals("No", QValueFormatter.formatValue(new QFieldMetaData().withType(QFieldType.BOOLEAN), "false"));
assertEquals("true", QValueFormatter.formatValue(new QFieldMetaData().withType(QFieldType.STRING), "true"));
assertEquals("false", QValueFormatter.formatValue(new QFieldMetaData().withType(QFieldType.STRING), "false"));
assertNull(QValueFormatter.formatValue(new QFieldMetaData().withType(QFieldType.TIME), null)); assertNull(QValueFormatter.formatValue(new QFieldMetaData().withType(QFieldType.TIME), null));
assertEquals("5:00:00 AM", QValueFormatter.formatValue(new QFieldMetaData().withType(QFieldType.TIME), LocalTime.of(5, 0))); assertEquals("5:00:00 AM", QValueFormatter.formatValue(new QFieldMetaData().withType(QFieldType.TIME), LocalTime.of(5, 0)));

View File

@ -83,7 +83,7 @@ class SavedViewProcessTests extends BaseTest
runProcessInput.addValue("tableName", tableName); runProcessInput.addValue("tableName", tableName);
runProcessInput.addValue("viewJson", JsonUtils.toJson(new QQueryFilter(new QFilterCriteria("id", QCriteriaOperator.EQUALS, 47)))); runProcessInput.addValue("viewJson", JsonUtils.toJson(new QQueryFilter(new QFilterCriteria("id", QCriteriaOperator.EQUALS, 47))));
RunProcessOutput runProcessOutput = new RunProcessAction().execute(runProcessInput); RunProcessOutput runProcessOutput = new RunProcessAction().execute(runProcessInput);
List<QRecord> savedViewList = (List<QRecord>) runProcessOutput.getValues().get("savedViewList"); List<QRecord> savedViewList = (List<QRecord>) runProcessOutput.getValues().get("savedViewList");
assertEquals(1, savedViewList.size()); assertEquals(1, savedViewList.size());
savedViewId = savedViewList.get(0).getValueInteger("id"); savedViewId = savedViewList.get(0).getValueInteger("id");
assertNotNull(savedViewId); assertNotNull(savedViewId);
@ -104,7 +104,7 @@ class SavedViewProcessTests extends BaseTest
runProcessInput.setProcessName(QuerySavedViewProcess.getProcessMetaData().getName()); runProcessInput.setProcessName(QuerySavedViewProcess.getProcessMetaData().getName());
runProcessInput.addValue("tableName", tableName); runProcessInput.addValue("tableName", tableName);
RunProcessOutput runProcessOutput = new RunProcessAction().execute(runProcessInput); RunProcessOutput runProcessOutput = new RunProcessAction().execute(runProcessInput);
List<QRecord> savedViewList = (List<QRecord>) runProcessOutput.getValues().get("savedViewList"); List<QRecord> savedViewList = (List<QRecord>) runProcessOutput.getValues().get("savedViewList");
assertEquals(1, savedViewList.size()); assertEquals(1, savedViewList.size());
assertEquals(1, savedViewList.get(0).getValueInteger("id")); assertEquals(1, savedViewList.get(0).getValueInteger("id"));
assertEquals("My View", savedViewList.get(0).getValueString("label")); assertEquals("My View", savedViewList.get(0).getValueString("label"));
@ -121,7 +121,7 @@ class SavedViewProcessTests extends BaseTest
runProcessInput.addValue("tableName", tableName); runProcessInput.addValue("tableName", tableName);
runProcessInput.addValue("viewJson", JsonUtils.toJson(new QQueryFilter(new QFilterCriteria("id", QCriteriaOperator.EQUALS, 47)))); runProcessInput.addValue("viewJson", JsonUtils.toJson(new QQueryFilter(new QFilterCriteria("id", QCriteriaOperator.EQUALS, 47))));
RunProcessOutput runProcessOutput = new RunProcessAction().execute(runProcessInput); RunProcessOutput runProcessOutput = new RunProcessAction().execute(runProcessInput);
List<QRecord> savedViewList = (List<QRecord>) runProcessOutput.getValues().get("savedViewList"); List<QRecord> savedViewList = (List<QRecord>) runProcessOutput.getValues().get("savedViewList");
assertEquals(1, savedViewList.size()); assertEquals(1, savedViewList.size());
assertEquals(1, savedViewList.get(0).getValueInteger("id")); assertEquals(1, savedViewList.get(0).getValueInteger("id"));
assertEquals("My Updated View", savedViewList.get(0).getValueString("label")); assertEquals("My Updated View", savedViewList.get(0).getValueString("label"));
@ -184,7 +184,64 @@ class SavedViewProcessTests extends BaseTest
RunProcessOutput runProcessOutput = new RunProcessAction().execute(runProcessInput); RunProcessOutput runProcessOutput = new RunProcessAction().execute(runProcessInput);
assertEquals(0, ((List<?>) runProcessOutput.getValues().get("savedViewList")).size()); assertEquals(0, ((List<?>) runProcessOutput.getValues().get("savedViewList")).size());
} }
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testNotFoundThrowsProperly() throws QException
{
QInstance qInstance = QContext.getQInstance();
new SavedViewsMetaDataProvider().defineAll(qInstance, TestUtils.MEMORY_BACKEND_NAME, null);
String tableName = TestUtils.TABLE_NAME_PERSON_MEMORY;
{
////////////////////////////////////////////////////////
// get one by id when it doesn't exist - should throw //
////////////////////////////////////////////////////////
RunProcessInput runProcessInput = new RunProcessInput();
runProcessInput.setProcessName(QuerySavedViewProcess.getProcessMetaData().getName());
runProcessInput.addValue("tableName", tableName);
runProcessInput.addValue("id", -1);
assertThatThrownBy(() -> new RunProcessAction().execute(runProcessInput))
.hasMessageContaining("view was not found")
.isInstanceOf(QUserFacingException.class);
}
Integer savedViewId;
{
//////////////////////
// store a new view //
//////////////////////
RunProcessInput runProcessInput = new RunProcessInput();
runProcessInput.setProcessName(StoreSavedViewProcess.getProcessMetaData().getName());
runProcessInput.addValue("label", "My View");
runProcessInput.addValue("tableName", tableName);
runProcessInput.addValue("viewJson", JsonUtils.toJson(new QQueryFilter(new QFilterCriteria("id", QCriteriaOperator.EQUALS, 47))));
RunProcessOutput runProcessOutput = new RunProcessAction().execute(runProcessInput);
List<QRecord> savedViewList = (List<QRecord>) runProcessOutput.getValues().get("savedViewList");
assertEquals(1, savedViewList.size());
savedViewId = savedViewList.get(0).getValueInteger("id");
assertNotNull(savedViewId);
}
{
////////////////////////////////////////
// get now with valid id, should work //
////////////////////////////////////////
RunProcessInput runProcessInput = new RunProcessInput();
runProcessInput.setProcessName(QuerySavedViewProcess.getProcessMetaData().getName());
runProcessInput.addValue("tableName", tableName);
runProcessInput.addValue("id", savedViewId);
RunProcessOutput runProcessOutput = new RunProcessAction().execute(runProcessInput);
List<QRecord> savedViewList = (List<QRecord>) runProcessOutput.getValues().get("savedViewList");
assertEquals(1, savedViewList.size());
assertEquals(1, savedViewList.get(0).getValueInteger("id"));
assertEquals("My View", savedViewList.get(0).getValueString("label"));
}
} }
} }

View File

@ -0,0 +1,76 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. 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.utils;
import java.util.Map;
import com.kingsrook.qqq.backend.core.BaseTest;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
/*******************************************************************************
** Unit test for CountingHash
*******************************************************************************/
class CountingHashTest extends BaseTest
{
/*******************************************************************************
**
*******************************************************************************/
@Test
void test()
{
CountingHash<String> countingHash = new CountingHash<>();
assertNull(countingHash.get("a"));
countingHash.add("a");
assertEquals(1, countingHash.get("a"));
countingHash.add("a");
assertEquals(2, countingHash.get("a"));
countingHash.add("a", 2);
assertEquals(4, countingHash.get("a"));
countingHash.add("b", 5);
assertEquals(5, countingHash.get("b"));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testAlwaysMutable()
{
CountingHash<String> alwaysMutable = new CountingHash<>(Map.of("A", 5));
alwaysMutable.add("A");
alwaysMutable.add("B");
assertEquals(6, alwaysMutable.get("A"));
assertEquals(1, alwaysMutable.get("B"));
}
}

View File

@ -737,20 +737,7 @@ public class BaseAPIActionUtil
case API_KEY_HEADER -> request.setHeader("API-Key", backendMetaData.getApiKey()); case API_KEY_HEADER -> request.setHeader("API-Key", backendMetaData.getApiKey());
case API_TOKEN -> request.setHeader("Authorization", "Token " + backendMetaData.getApiKey()); case API_TOKEN -> request.setHeader("Authorization", "Token " + backendMetaData.getApiKey());
case OAUTH2 -> request.setHeader("Authorization", "Bearer " + getOAuth2Token()); case OAUTH2 -> request.setHeader("Authorization", "Bearer " + getOAuth2Token());
case API_KEY_QUERY_PARAM -> case API_KEY_QUERY_PARAM -> addApiKeyQueryParamToRequest(request);
{
try
{
String uri = request.getURI().toString();
uri += (uri.contains("?") ? "&" : "?");
uri += backendMetaData.getApiKeyQueryParamName() + "=" + backendMetaData.getApiKey();
request.setURI(new URI(uri));
}
catch(URISyntaxException e)
{
throw (new QException("Error setting authorization query parameter", e));
}
}
case CUSTOM -> case CUSTOM ->
{ {
handleCustomAuthorization(request); handleCustomAuthorization(request);
@ -761,6 +748,35 @@ public class BaseAPIActionUtil
/***************************************************************************
**
***************************************************************************/
protected void addApiKeyQueryParamToRequest(HttpRequestBase request) throws QException
{
try
{
String uri = request.getURI().toString();
String pair = backendMetaData.getApiKeyQueryParamName() + "=" + backendMetaData.getApiKey();
///////////////////////////////////////////////////////////////////////////////////
// avoid re-adding the name=value pair if it's already there (e.g., for a retry) //
///////////////////////////////////////////////////////////////////////////////////
if(!uri.contains(pair))
{
uri += (uri.contains("?") ? "&" : "?");
uri += pair;
}
request.setURI(new URI(uri));
}
catch(URISyntaxException e)
{
throw (new QException("Error setting authorization query parameter", e));
}
}
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
@ -1210,39 +1226,11 @@ public class BaseAPIActionUtil
return; return;
} }
String requestBody = null; OutboundAPILog outboundAPILog = generateOutboundApiLogRecord(request, response);
if(request instanceof HttpEntityEnclosingRequest entityRequest)
{
try
{
requestBody = StringUtils.join("\n", IOUtils.readLines(entityRequest.getEntity().getContent(), StandardCharsets.UTF_8));
}
catch(Exception e)
{
// leave it null...
}
}
////////////////////////////////////
// mask api keys in query strings //
////////////////////////////////////
String url = request.getURI().toString();
if(backendMetaData.getAuthorizationType().equals(AuthorizationType.API_KEY_QUERY_PARAM))
{
url = url.replaceFirst(backendMetaData.getApiKey(), "******");
}
InsertInput insertInput = new InsertInput(); InsertInput insertInput = new InsertInput();
insertInput.setTableName(table.getName()); insertInput.setTableName(table.getName());
insertInput.setRecords(List.of(new OutboundAPILog() insertInput.setRecords(List.of(outboundAPILog.toQRecord()));
.withMethod(request.getMethod())
.withUrl(url)
.withTimestamp(Instant.now())
.withRequestBody(requestBody)
.withStatusCode(response.getStatusCode())
.withResponseBody(response.getContent())
.toQRecord()
));
new InsertAction().executeAsync(insertInput); new InsertAction().executeAsync(insertInput);
} }
catch(Exception e) catch(Exception e)
@ -1253,6 +1241,44 @@ public class BaseAPIActionUtil
/***************************************************************************
**
***************************************************************************/
public OutboundAPILog generateOutboundApiLogRecord(HttpRequestBase request, QHttpResponse response)
{
String requestBody = null;
if(request instanceof HttpEntityEnclosingRequest entityRequest)
{
try
{
requestBody = StringUtils.join("\n", IOUtils.readLines(entityRequest.getEntity().getContent(), StandardCharsets.UTF_8));
}
catch(Exception e)
{
// leave it null...
}
}
////////////////////////////////////
// mask api keys in query strings //
////////////////////////////////////
String url = request.getURI().toString();
if(backendMetaData.getAuthorizationType().equals(AuthorizationType.API_KEY_QUERY_PARAM))
{
url = url.replaceAll(backendMetaData.getApiKey(), "******");
}
return new OutboundAPILog()
.withMethod(request.getMethod())
.withUrl(url)
.withTimestamp(Instant.now())
.withRequestBody(requestBody)
.withStatusCode(response.getStatusCode())
.withResponseBody(response.getContent());
}
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/

View File

@ -61,6 +61,7 @@ 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.model.metadata.tables.UniqueKey;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.SleepUtils; import com.kingsrook.qqq.backend.core.utils.SleepUtils;
import com.kingsrook.qqq.backend.core.utils.lambdas.UnsafeConsumer;
import com.kingsrook.qqq.backend.module.api.BaseTest; import com.kingsrook.qqq.backend.module.api.BaseTest;
import com.kingsrook.qqq.backend.module.api.TestUtils; import com.kingsrook.qqq.backend.module.api.TestUtils;
import com.kingsrook.qqq.backend.module.api.exceptions.RateLimitException; import com.kingsrook.qqq.backend.module.api.exceptions.RateLimitException;
@ -74,6 +75,7 @@ import org.apache.http.Header;
import org.apache.http.HttpEntity; import org.apache.http.HttpEntity;
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase; import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpRequestBase;
import org.json.JSONArray; import org.json.JSONArray;
import org.json.JSONObject; import org.json.JSONObject;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
@ -868,6 +870,72 @@ class BaseAPIActionUtilTest extends BaseTest
/*******************************************************************************
**
*******************************************************************************/
@Test
void testAuthorizationApiKeyQueryParam() throws QException
{
APIBackendMetaData backend = (APIBackendMetaData) QContext.getQInstance().getBackend(TestUtils.MOCK_BACKEND_NAME);
backend.setAuthorizationType(AuthorizationType.API_KEY_QUERY_PARAM);
backend.setApiKeyQueryParamName("apikey");
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);
//////////////////////////////////////////////////////////////////////////////
// we'll want to assert that the URL has the api query string - and just //
// one copy of it (as we once had a bug where it got duplicated upon retry) //
//////////////////////////////////////////////////////////////////////////////
UnsafeConsumer<HttpRequestBase, Exception> asserter = request -> assertThat(request.getURI().toString())
.contains("?apikey=9876-WXYZ")
.doesNotContain("?apikey=9876-WXYZ&apikey=9876-WXYZ");
////////////////////////////////////////
// queue up a 429, so we'll try-again //
////////////////////////////////////////
mockApiUtilsHelper.setMockRequestAsserter(asserter);
mockApiUtilsHelper.enqueueMockResponse(new QHttpResponse().withStatusCode(429).withContent(""));
//////////////////////
// queue a response //
//////////////////////
mockApiUtilsHelper.setMockRequestAsserter(asserter);
mockApiUtilsHelper.enqueueMockResponse("""
{"id": 3, "name": "Bart"},
""");
GetOutput getOutput = runSimpleGetAction();
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testGenerateOutboundApiLogRecord() throws QException
{
APIBackendMetaData backend = (APIBackendMetaData) QContext.getQInstance().getBackend(TestUtils.MOCK_BACKEND_NAME);
backend.setAuthorizationType(AuthorizationType.API_KEY_QUERY_PARAM);
backend.setApiKeyQueryParamName("apikey");
backend.setApiKey("9876-WXYZ");
MockApiActionUtils mockApiActionUtils = new MockApiActionUtils();
mockApiActionUtils.setBackendMetaData(backend);
OutboundAPILog outboundAPILog = mockApiActionUtils.generateOutboundApiLogRecord(new HttpGet("...?apikey=9876-WXYZ"), new QHttpResponse());
assertThat(outboundAPILog.getUrl())
.doesNotContain("9876-WXYZ")
.contains("?apikey=*****");
}
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/

View File

@ -661,7 +661,7 @@ public abstract class AbstractRDBMSAction
} }
else if(!expectedNoOfParams.equals(values.size())) else if(!expectedNoOfParams.equals(values.size()))
{ {
throw new IllegalArgumentException("Incorrect number of values given for criteria [" + field.getName() + "]"); throw new IllegalArgumentException("Incorrect number of values given for criteria [" + field.getName() + "] (expected " + expectedNoOfParams + ", received " + values.size() + ")");
} }
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

View File

@ -25,6 +25,7 @@ package com.kingsrook.qqq.backend.module.rdbms.actions;
import java.io.Serializable; import java.io.Serializable;
import java.sql.Connection; import java.sql.Connection;
import java.sql.ResultSet; import java.sql.ResultSet;
import java.time.Instant;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -117,6 +118,14 @@ public class RDBMSAggregateAction extends AbstractRDBMSAction implements Aggrega
actionTimeoutHelper = new ActionTimeoutHelper(aggregateInput.getTimeoutSeconds(), TimeUnit.SECONDS, new StatementTimeoutCanceller(statement, sql)); actionTimeoutHelper = new ActionTimeoutHelper(aggregateInput.getTimeoutSeconds(), TimeUnit.SECONDS, new StatementTimeoutCanceller(statement, sql));
actionTimeoutHelper.start(); actionTimeoutHelper.start();
///////////////////////////////////////////////////////////////////////////////////////////////////
// to avoid counting time spent acquiring a connection, re-set the queryStat startTimestamp here //
///////////////////////////////////////////////////////////////////////////////////////////////////
if(queryStat != null)
{
queryStat.setStartTimestamp(Instant.now());
}
QueryManager.executeStatement(statement, sql, ((ResultSet resultSet) -> QueryManager.executeStatement(statement, sql, ((ResultSet resultSet) ->
{ {
///////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////

View File

@ -25,6 +25,7 @@ package com.kingsrook.qqq.backend.module.rdbms.actions;
import java.io.Serializable; import java.io.Serializable;
import java.sql.Connection; import java.sql.Connection;
import java.sql.ResultSet; import java.sql.ResultSet;
import java.time.Instant;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -97,6 +98,14 @@ public class RDBMSCountAction extends AbstractRDBMSAction implements CountInterf
actionTimeoutHelper = new ActionTimeoutHelper(countInput.getTimeoutSeconds(), TimeUnit.SECONDS, new StatementTimeoutCanceller(statement, sql)); actionTimeoutHelper = new ActionTimeoutHelper(countInput.getTimeoutSeconds(), TimeUnit.SECONDS, new StatementTimeoutCanceller(statement, sql));
actionTimeoutHelper.start(); actionTimeoutHelper.start();
///////////////////////////////////////////////////////////////////////////////////////////////////
// to avoid counting time spent acquiring a connection, re-set the queryStat startTimestamp here //
///////////////////////////////////////////////////////////////////////////////////////////////////
if(queryStat != null)
{
queryStat.setStartTimestamp(Instant.now());
}
QueryManager.executeStatement(statement, sql, ((ResultSet resultSet) -> QueryManager.executeStatement(statement, sql, ((ResultSet resultSet) ->
{ {
///////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////

View File

@ -28,6 +28,7 @@ import java.sql.PreparedStatement;
import java.sql.ResultSet; import java.sql.ResultSet;
import java.sql.ResultSetMetaData; import java.sql.ResultSetMetaData;
import java.sql.SQLException; import java.sql.SQLException;
import java.time.Instant;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
@ -166,6 +167,14 @@ public class RDBMSQueryAction extends AbstractRDBMSAction implements QueryInterf
actionTimeoutHelper = new ActionTimeoutHelper(queryInput.getTimeoutSeconds(), TimeUnit.SECONDS, new StatementTimeoutCanceller(statement, sql)); actionTimeoutHelper = new ActionTimeoutHelper(queryInput.getTimeoutSeconds(), TimeUnit.SECONDS, new StatementTimeoutCanceller(statement, sql));
actionTimeoutHelper.start(); actionTimeoutHelper.start();
///////////////////////////////////////////////////////////////////////////////////////////////////
// to avoid counting time spent acquiring a connection, re-set the queryStat startTimestamp here //
///////////////////////////////////////////////////////////////////////////////////////////////////
if(queryStat != null)
{
queryStat.setStartTimestamp(Instant.now());
}
////////////////////////////////////////////// //////////////////////////////////////////////
// execute the query - iterate over results // // execute the query - iterate over results //
////////////////////////////////////////////// //////////////////////////////////////////////