Update ApiQueryFilterUtils.manageCriteriaFields with basic support filtering by an exposed join.

This commit is contained in:
2025-07-02 08:50:16 -05:00
parent b3f5f6bfc1
commit b5134cd0c6
4 changed files with 140 additions and 5 deletions

View File

@ -82,7 +82,7 @@ public class ApiAwareTableCountExecutor extends TableCountExecutor implements Ap
// take care of managing criteria, which may not be in this version, etc // // take care of managing criteria, which may not be in this version, etc //
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
QQueryFilter filter = Objects.requireNonNullElseGet(input.getFilter(), () -> new QQueryFilter()); QQueryFilter filter = Objects.requireNonNullElseGet(input.getFilter(), () -> new QQueryFilter());
ApiQueryFilterUtils.manageCriteriaFields(filter, tableApiFields, badRequestMessages, apiName, countInput); ApiQueryFilterUtils.manageCriteriaFields(filter, tableApiFields, badRequestMessages, apiName, apiVersion, countInput);
////////////////////////////////////////// //////////////////////////////////////////
// no more badRequest checks below here // // no more badRequest checks below here //

View File

@ -106,7 +106,7 @@ public class ApiAwareTableQueryExecutor extends TableQueryExecutor implements Ap
// take care of managing order-by fields and criteria, which may not be in this version, etc // // take care of managing order-by fields and criteria, which may not be in this version, etc //
/////////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////////
manageOrderByFields(filter, tableApiFields, badRequestMessages, apiName, queryInput); manageOrderByFields(filter, tableApiFields, badRequestMessages, apiName, queryInput);
ApiQueryFilterUtils.manageCriteriaFields(filter, tableApiFields, badRequestMessages, apiName, queryInput); ApiQueryFilterUtils.manageCriteriaFields(filter, tableApiFields, badRequestMessages, apiName, apiVersion, queryInput);
////////////////////////////////////////// //////////////////////////////////////////
// no more badRequest checks below here // // no more badRequest checks below here //

View File

@ -24,11 +24,14 @@ package com.kingsrook.qqq.api.utils;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import com.kingsrook.qqq.api.actions.GetTableApiFieldsAction;
import com.kingsrook.qqq.api.model.actions.ApiFieldCustomValueMapper; import com.kingsrook.qqq.api.model.actions.ApiFieldCustomValueMapper;
import com.kingsrook.qqq.api.model.metadata.fields.ApiFieldMetaData; 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.fields.ApiFieldMetaDataContainer;
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader; import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
import com.kingsrook.qqq.backend.core.exceptions.QBadRequestException; import com.kingsrook.qqq.backend.core.exceptions.QBadRequestException;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.tables.QueryOrCountInputInterface; import com.kingsrook.qqq.backend.core.model.actions.tables.QueryOrCountInputInterface;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
@ -36,6 +39,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.ObjectUtils; import com.kingsrook.qqq.backend.core.utils.ObjectUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/******************************************************************************* /*******************************************************************************
@ -43,16 +47,57 @@ import com.kingsrook.qqq.backend.core.utils.StringUtils;
*******************************************************************************/ *******************************************************************************/
public class ApiQueryFilterUtils public class ApiQueryFilterUtils
{ {
private static final QLogger LOG = QLogger.getLogger(ApiQueryFilterUtils.class);
/*************************************************************************** /***************************************************************************
** **
***************************************************************************/ ***************************************************************************/
public static void manageCriteriaFields(QQueryFilter filter, Map<String, QFieldMetaData> tableApiFields, List<String> badRequestMessages, String apiName, QueryOrCountInputInterface input) @Deprecated(since = "version was added that took apiVerison")
public static void manageCriteriaFields(QQueryFilter filter, Map<String, QFieldMetaData> tableApiFields, List<String> badRequestMessages, String apiName, QueryOrCountInputInterface input) throws QException
{
manageCriteriaFields(filter, tableApiFields, badRequestMessages, apiName, null, input);
}
/***************************************************************************
**
***************************************************************************/
public static void manageCriteriaFields(QQueryFilter filter, Map<String, QFieldMetaData> tableApiFields, List<String> badRequestMessages, String apiName, String apiVersion, QueryOrCountInputInterface input) throws QException
{ {
for(QFilterCriteria criteria : CollectionUtils.nonNullList(filter.getCriteria())) for(QFilterCriteria criteria : CollectionUtils.nonNullList(filter.getCriteria()))
{ {
String apiFieldName = criteria.getFieldName(); String apiFieldName = criteria.getFieldName();
QFieldMetaData field = tableApiFields.get(apiFieldName); QFieldMetaData field = tableApiFields.get(apiFieldName);
String joinTableName = null;
if(apiFieldName.contains("."))
{
if(apiVersion == null)
{
LOG.warn("No apiVersion provided for manageCriteriaFields. Cannot process join criteria field", logPair("fieldName", apiFieldName));
badRequestMessages.add("Cannot process joined criteria field: " + apiFieldName);
continue;
}
try
{
String[] split = apiFieldName.split("\\.", 2);
joinTableName = split[0];
String joinFieldName = split[1];
Map<String, QFieldMetaData> joinTableApiFields = GetTableApiFieldsAction.getTableApiFieldMap(new GetTableApiFieldsAction.ApiNameVersionAndTableName(apiName, apiVersion, joinTableName));
field = joinTableApiFields.get(joinFieldName);
}
catch(Exception e)
{
badRequestMessages.add("Error processing criteria field: " + apiFieldName + ": " + e.getMessage());
continue;
}
}
if(field == null) if(field == null)
{ {
badRequestMessages.add("Unrecognized criteria field name: " + apiFieldName + "."); badRequestMessages.add("Unrecognized criteria field name: " + apiFieldName + ".");
@ -61,10 +106,12 @@ public class ApiQueryFilterUtils
{ {
try try
{ {
ApiFieldMetaData apiFieldMetaData = ObjectUtils.tryAndRequireNonNullElse(() -> ApiFieldMetaDataContainer.of(field).getApiFieldMetaData(apiName), new ApiFieldMetaData()); QFieldMetaData finalField = field;
ApiFieldMetaData apiFieldMetaData = ObjectUtils.tryAndRequireNonNullElse(() -> ApiFieldMetaDataContainer.of(finalField).getApiFieldMetaData(apiName), new ApiFieldMetaData());
if(StringUtils.hasContent(apiFieldMetaData.getReplacedByFieldName())) if(StringUtils.hasContent(apiFieldMetaData.getReplacedByFieldName()))
{ {
criteria.setFieldName(apiFieldMetaData.getReplacedByFieldName()); String joinTablePrefix = joinTableName == null ? "" : (joinTableName + ".");
criteria.setFieldName(joinTablePrefix + apiFieldMetaData.getReplacedByFieldName());
} }
else if(apiFieldMetaData.getCustomValueMapper() != null) else if(apiFieldMetaData.getCustomValueMapper() != null)
{ {

View File

@ -23,11 +23,15 @@ package com.kingsrook.qqq.api.middleware.specs.v1;
import java.util.Map; import java.util.Map;
import java.util.function.BiConsumer;
import com.kingsrook.qqq.api.TestUtils; import com.kingsrook.qqq.api.TestUtils;
import com.kingsrook.qqq.api.middleware.specs.ApiAwareSpecTestBase; import com.kingsrook.qqq.api.middleware.specs.ApiAwareSpecTestBase;
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.query.QCriteriaOperator; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.session.QSystemUserSession;
import com.kingsrook.qqq.backend.core.utils.JsonUtils; import com.kingsrook.qqq.backend.core.utils.JsonUtils;
import com.kingsrook.qqq.middleware.javalin.specs.AbstractEndpointSpec; import com.kingsrook.qqq.middleware.javalin.specs.AbstractEndpointSpec;
import io.javalin.http.ContentType; import io.javalin.http.ContentType;
@ -270,4 +274,88 @@ class ApiAwareTableQuerySpecV1Test extends ApiAwareSpecTestBase
assertEquals("Could not find a table named no-such-table in this api.", jsonObject.getString("error")); assertEquals("Could not find a table named no-such-table in this api.", jsonObject.getString("error"));
} }
/*******************************************************************************
**
*******************************************************************************/
@Test
void testJoin() throws QException
{
/////////////////////////
// insert a test order //
/////////////////////////
QContext.init(TestUtils.defineInstance(), new QSystemUserSession());
TestUtils.insert1Order3Lines4LineExtrinsicsAnd1OrderExtrinsic();
/////////////////////////////
// assert success function //
/////////////////////////////
BiConsumer<HttpResponse<String>, Integer> assertOrderCount = (response, expectedCount) ->
{
assertEquals(200, response.getStatus());
JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
JSONArray records = jsonObject.getJSONArray("records");
assertThat(records.length()).isEqualTo(expectedCount);
};
/////////////////////////
// assert 400 function //
/////////////////////////
BiConsumer<HttpResponse<String>, String> assert400 = (response, expectedMessage) ->
{
assertEquals(400, response.getStatus());
JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
assertEquals(expectedMessage, jsonObject.getString("error"));
};
////////////////////////////////////////////////////////////
// basic query (with no join filter) should find 1 record //
////////////////////////////////////////////////////////////
HttpResponse<String> response;
response = Unirest.post(getBaseUrlAndPath(TestUtils.API_PATH, TestUtils.V2023_Q1) + "/table/order/query")
.body(JsonUtils.toJson(Map.of("filter", new QQueryFilter(new QFilterCriteria("orderNo", QCriteriaOperator.EQUALS, "ORD123")))))
.contentType(ContentType.APPLICATION_JSON.getMimeType())
.asString();
assertOrderCount.accept(response, 1);
//////////////////////////////////////////////////////////////////
// basic query (with no join filter) that should find 0 records //
//////////////////////////////////////////////////////////////////
response = Unirest.post(getBaseUrlAndPath(TestUtils.API_PATH, TestUtils.V2023_Q1) + "/table/order/query")
.body(JsonUtils.toJson(Map.of("filter", new QQueryFilter(new QFilterCriteria("orderNo", QCriteriaOperator.EQUALS, "not-found")))))
.contentType(ContentType.APPLICATION_JSON.getMimeType())
.asString();
assertOrderCount.accept(response, 0);
///////////////////////////////////////////////////////////
// try to filter by unknown join-table name - should 400 //
///////////////////////////////////////////////////////////
response = Unirest.post(getBaseUrlAndPath(TestUtils.API_PATH, TestUtils.V2023_Q1) + "/table/order/query")
.body(JsonUtils.toJson(Map.of("filter", new QQueryFilter(new QFilterCriteria("noSuchTable.sku", QCriteriaOperator.EQUALS, "BASIC1")))))
.contentType(ContentType.APPLICATION_JSON.getMimeType())
.asString();
assert400.accept(response, "Error processing criteria field: noSuchTable.sku: Unrecognized table name: noSuchTable");
///////////////////////////////////////////////////////////
// try to filter by unknown join field name - should 400 //
///////////////////////////////////////////////////////////
response = Unirest.post(getBaseUrlAndPath(TestUtils.API_PATH, TestUtils.V2023_Q1) + "/table/order/query")
.body(JsonUtils.toJson(Map.of("filter", new QQueryFilter(new QFilterCriteria(TestUtils.TABLE_NAME_LINE_ITEM + ".noSuchField", QCriteriaOperator.EQUALS, "BASIC1")))))
.contentType(ContentType.APPLICATION_JSON.getMimeType())
.asString();
assert400.accept(response, "Unrecognized criteria field name: orderLine.noSuchField.");
///////////////////////////////////////////////////////////////////////////////////////////////
// join for sku - should find (but... memory backend isn't joining correctly at this time... //
// so we'll ensure at least http 200, and trust that other backends join correctly... //
///////////////////////////////////////////////////////////////////////////////////////////////
response = Unirest.post(getBaseUrlAndPath(TestUtils.API_PATH, TestUtils.V2023_Q1) + "/table/order/query")
.body(JsonUtils.toJson(Map.of("filter", new QQueryFilter(new QFilterCriteria(TestUtils.TABLE_NAME_LINE_ITEM + ".sku", QCriteriaOperator.EQUALS, "BASIC1")))))
.contentType(ContentType.APPLICATION_JSON.getMimeType())
.asString();
assertOrderCount.accept(response, 0); // todo - ideally 1, but memory backend joining...
}
} }