From d7867b8d22996d7fe0b49b6fdfeaeb25970c55aa Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Thu, 19 Jun 2025 14:49:07 -0500 Subject: [PATCH 1/2] replace all relative program paths (e.g., cp) with absolute ones (e.g., /bin/cp), in constants (e.g., CP); --- .../com/kingsrook/qqq/devtools/CreateNewQBit.java | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/qqq-dev-tools/src/main/java/com/kingsrook/qqq/devtools/CreateNewQBit.java b/qqq-dev-tools/src/main/java/com/kingsrook/qqq/devtools/CreateNewQBit.java index 9d5a7194..e37e02df 100644 --- a/qqq-dev-tools/src/main/java/com/kingsrook/qqq/devtools/CreateNewQBit.java +++ b/qqq-dev-tools/src/main/java/com/kingsrook/qqq/devtools/CreateNewQBit.java @@ -21,7 +21,10 @@ public class CreateNewQBit private static ExecutorService executorService = null; - private static String SED = "/opt/homebrew/bin/gsed"; + private static String SED = "/opt/homebrew/bin/gsed"; // needs to be a version that supports -i (in-place edit) + private static String GIT = "/usr/bin/git"; + private static String CP = "/bin/cp"; + private static String MV = "/bin/mv"; @@ -87,7 +90,7 @@ public class CreateNewQBit System.out.println(); System.out.println("Copying template..."); - ProcessResult cpResult = run(new ProcessBuilder("cp", "-rv", template.getAbsolutePath(), dir.getAbsolutePath())); + ProcessResult cpResult = run(new ProcessBuilder(CP, "-rv", template.getAbsolutePath(), dir.getAbsolutePath())); System.out.print(cpResult.stdout()); System.out.println(); @@ -100,7 +103,7 @@ public class CreateNewQBit System.out.println(); System.out.println("Init'ing git repo..."); - run(new ProcessBuilder("git", "init").directory(dir)); + run(new ProcessBuilder(GIT, "init").directory(dir)); System.out.println(); // git remote add origin https://github.com/Kingsrook/${name}.git ? @@ -123,9 +126,9 @@ public class CreateNewQBit { String srcPath = dir.getAbsolutePath() + "/src/main/java/com/kingsrook/qbits"; String packagePath = packageName.replace('.', '/'); - System.out.print(run(new ProcessBuilder("mv", "-v", srcPath + "/todo/TodoQBitConfig.java", srcPath + "/todo/" + className + "QBitConfig.java")).stdout()); - System.out.print(run(new ProcessBuilder("mv", "-v", srcPath + "/todo/TodoQBitProducer.java", srcPath + "/todo/" + className + "QBitProducer.java")).stdout()); - System.out.print(run(new ProcessBuilder("mv", "-v", srcPath + "/todo", srcPath + "/" + packagePath)).stdout()); + System.out.print(run(new ProcessBuilder(MV, "-v", srcPath + "/todo/TodoQBitConfig.java", srcPath + "/todo/" + className + "QBitConfig.java")).stdout()); + System.out.print(run(new ProcessBuilder(MV, "-v", srcPath + "/todo/TodoQBitProducer.java", srcPath + "/todo/" + className + "QBitProducer.java")).stdout()); + System.out.print(run(new ProcessBuilder(MV, "-v", srcPath + "/todo", srcPath + "/" + packagePath)).stdout()); } From b5134cd0c6efc22c4e3736baf2aaade08a3fdb2a Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Wed, 2 Jul 2025 08:50:16 -0500 Subject: [PATCH 2/2] Update ApiQueryFilterUtils.manageCriteriaFields with basic support filtering by an exposed join. --- .../executors/ApiAwareTableCountExecutor.java | 2 +- .../executors/ApiAwareTableQueryExecutor.java | 2 +- .../qqq/api/utils/ApiQueryFilterUtils.java | 53 ++++++++++- .../v1/ApiAwareTableQuerySpecV1Test.java | 88 +++++++++++++++++++ 4 files changed, 140 insertions(+), 5 deletions(-) diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/middleware/executors/ApiAwareTableCountExecutor.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/middleware/executors/ApiAwareTableCountExecutor.java index 99ed5c07..049cab14 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/middleware/executors/ApiAwareTableCountExecutor.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/middleware/executors/ApiAwareTableCountExecutor.java @@ -82,7 +82,7 @@ public class ApiAwareTableCountExecutor extends TableCountExecutor implements Ap // take care of managing criteria, which may not be in this version, etc // /////////////////////////////////////////////////////////////////////////// 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 // diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/middleware/executors/ApiAwareTableQueryExecutor.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/middleware/executors/ApiAwareTableQueryExecutor.java index 909bbc34..4c40bd21 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/middleware/executors/ApiAwareTableQueryExecutor.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/middleware/executors/ApiAwareTableQueryExecutor.java @@ -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 // /////////////////////////////////////////////////////////////////////////////////////////////// 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 // diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/utils/ApiQueryFilterUtils.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/utils/ApiQueryFilterUtils.java index 89528eab..906d1d13 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/utils/ApiQueryFilterUtils.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/utils/ApiQueryFilterUtils.java @@ -24,11 +24,14 @@ package com.kingsrook.qqq.api.utils; import java.util.List; 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.metadata.fields.ApiFieldMetaData; import com.kingsrook.qqq.api.model.metadata.fields.ApiFieldMetaDataContainer; import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader; 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.query.QFilterCriteria; 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.ObjectUtils; 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 { + private static final QLogger LOG = QLogger.getLogger(ApiQueryFilterUtils.class); + + /*************************************************************************** ** ***************************************************************************/ - public static void manageCriteriaFields(QQueryFilter filter, Map tableApiFields, List badRequestMessages, String apiName, QueryOrCountInputInterface input) + @Deprecated(since = "version was added that took apiVerison") + public static void manageCriteriaFields(QQueryFilter filter, Map tableApiFields, List badRequestMessages, String apiName, QueryOrCountInputInterface input) throws QException + { + manageCriteriaFields(filter, tableApiFields, badRequestMessages, apiName, null, input); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + public static void manageCriteriaFields(QQueryFilter filter, Map tableApiFields, List badRequestMessages, String apiName, String apiVersion, QueryOrCountInputInterface input) throws QException { for(QFilterCriteria criteria : CollectionUtils.nonNullList(filter.getCriteria())) { String apiFieldName = criteria.getFieldName(); 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 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) { badRequestMessages.add("Unrecognized criteria field name: " + apiFieldName + "."); @@ -61,10 +106,12 @@ public class ApiQueryFilterUtils { 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())) { - criteria.setFieldName(apiFieldMetaData.getReplacedByFieldName()); + String joinTablePrefix = joinTableName == null ? "" : (joinTableName + "."); + criteria.setFieldName(joinTablePrefix + apiFieldMetaData.getReplacedByFieldName()); } else if(apiFieldMetaData.getCustomValueMapper() != null) { diff --git a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/middleware/specs/v1/ApiAwareTableQuerySpecV1Test.java b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/middleware/specs/v1/ApiAwareTableQuerySpecV1Test.java index b0a842e8..34f81de4 100644 --- a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/middleware/specs/v1/ApiAwareTableQuerySpecV1Test.java +++ b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/middleware/specs/v1/ApiAwareTableQuerySpecV1Test.java @@ -23,11 +23,15 @@ package com.kingsrook.qqq.api.middleware.specs.v1; import java.util.Map; +import java.util.function.BiConsumer; import com.kingsrook.qqq.api.TestUtils; 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.QFilterCriteria; 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.middleware.javalin.specs.AbstractEndpointSpec; 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")); } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testJoin() throws QException + { + ///////////////////////// + // insert a test order // + ///////////////////////// + QContext.init(TestUtils.defineInstance(), new QSystemUserSession()); + TestUtils.insert1Order3Lines4LineExtrinsicsAnd1OrderExtrinsic(); + + ///////////////////////////// + // assert success function // + ///////////////////////////// + BiConsumer, 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, 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 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... + + } + } \ No newline at end of file