diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/GetAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/GetAction.java index 69311617..84a6ec91 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/GetAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/GetAction.java @@ -329,6 +329,8 @@ public class GetAction } queryInput.setFilter(filter); + queryInput.setIncludeAssociations(getInput.getIncludeAssociations()); + queryInput.setAssociationNamesToInclude(getInput.getAssociationNamesToInclude()); queryInput.setShouldFetchHeavyFields(getInput.getShouldFetchHeavyFields()); QueryOutput queryOutput = new QueryAction().execute(queryInput); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/QueryAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/QueryAction.java index a03f2ad8..11763ab8 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/QueryAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/QueryAction.java @@ -22,8 +22,13 @@ package com.kingsrook.qqq.backend.core.actions.tables; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; import java.util.List; import java.util.Optional; +import java.util.Set; import com.kingsrook.qqq.backend.core.actions.ActionHelper; import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPostQueryCustomizer; import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader; @@ -31,13 +36,23 @@ 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.values.QPossibleValueTranslator; import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter; +import com.kingsrook.qqq.backend.core.context.QContext; 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.query.QCriteriaOperator; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput; import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn; +import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.tables.Association; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher; import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import com.kingsrook.qqq.backend.core.utils.ListingHash; /******************************************************************************* @@ -70,6 +85,14 @@ public class QueryAction 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.")); + } + QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher(); QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(queryInput.getBackend()); // todo pre-customization - just get to modify the request? @@ -86,11 +109,119 @@ public class QueryAction postRecordActions(queryOutput.getRecords()); } + if(queryInput.getIncludeAssociations()) + { + manageAssociations(queryInput, queryOutput); + } + return queryOutput; } + /******************************************************************************* + ** + *******************************************************************************/ + private void manageAssociations(QueryInput queryInput, QueryOutput queryOutput) throws QException + { + QTableMetaData table = queryInput.getTable(); + for(Association association : CollectionUtils.nonNullList(table.getAssociations())) + { + if(queryInput.getAssociationNamesToInclude() == null || queryInput.getAssociationNamesToInclude().contains(association.getName())) + { + // e.g., order -> orderLine + QJoinMetaData join = QContext.getQInstance().getJoin(association.getJoinName()); // todo ... ever need to flip? + // just assume this, at least for now... if(BooleanUtils.isTrue(association.getDoInserts())) + + QueryInput nextLevelQueryInput = new QueryInput(); + nextLevelQueryInput.setTableName(association.getAssociatedTableName()); + nextLevelQueryInput.setIncludeAssociations(true); + nextLevelQueryInput.setAssociationNamesToInclude(buildNextLevelAssociationNamesToInclude(association.getName(), queryInput.getAssociationNamesToInclude())); + + QQueryFilter filter = new QQueryFilter(); + nextLevelQueryInput.setFilter(filter); + + ListingHash, QRecord> outerResultMap = new ListingHash<>(); + + if(join.getJoinOns().size() == 1) + { + JoinOn joinOn = join.getJoinOns().get(0); + Set values = new HashSet<>(); + for(QRecord record : queryOutput.getRecords()) + { + Serializable value = record.getValue(joinOn.getLeftField()); + values.add(value); + outerResultMap.add(List.of(value), record); + } + filter.addCriteria(new QFilterCriteria(joinOn.getRightField(), QCriteriaOperator.IN, new ArrayList<>(values))); + } + else + { + filter.setBooleanOperator(QQueryFilter.BooleanOperator.OR); + + for(QRecord record : queryOutput.getRecords()) + { + QQueryFilter subFilter = new QQueryFilter(); + filter.addSubFilter(subFilter); + List values = new ArrayList<>(); + for(JoinOn joinOn : join.getJoinOns()) + { + Serializable value = record.getValue(joinOn.getLeftField()); + values.add(value); + subFilter.addCriteria(new QFilterCriteria(joinOn.getRightField(), QCriteriaOperator.EQUALS, value)); + } + outerResultMap.add(values, record); + } + } + + QueryOutput nextLevelQueryOutput = new QueryAction().execute(nextLevelQueryInput); + for(QRecord record : nextLevelQueryOutput.getRecords()) + { + List values = new ArrayList<>(); + for(JoinOn joinOn : join.getJoinOns()) + { + Serializable value = record.getValue(joinOn.getRightField()); + values.add(value); + } + + if(outerResultMap.containsKey(values)) + { + for(QRecord outerRecord : outerResultMap.get(values)) + { + outerRecord.withAssociatedRecord(association.getName(), record); + } + } + } + } + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private Collection buildNextLevelAssociationNamesToInclude(String name, Collection associationNamesToInclude) + { + if(associationNamesToInclude == null) + { + return (associationNamesToInclude); + } + + Set rs = new HashSet<>(); + for(String nextLevelCandidateName : associationNamesToInclude) + { + if(nextLevelCandidateName.startsWith(name + ".")) + { + rs.add(nextLevelCandidateName.replaceFirst(name + ".", "")); + } + } + + return (rs); + } + + + /******************************************************************************* ** Run the necessary actions on a list of records (which must be a mutable list - e.g., ** not one created via List.of()). This may include setting display values, diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/get/GetInput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/get/GetInput.java index ada0fbea..3a9b2c35 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/get/GetInput.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/get/GetInput.java @@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.get; import java.io.Serializable; +import java.util.Collection; import java.util.Map; import com.kingsrook.qqq.backend.core.actions.QBackendTransaction; import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput; @@ -44,6 +45,15 @@ public class GetInput extends AbstractTableActionInput private boolean shouldFetchHeavyFields = true; + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // if you say you want to includeAssociations, you can limit which ones by passing them in associationNamesToInclude. // + // if you leave it null, you get all associations defined on the table. if you pass it as empty, you get none. // + // to go to a recursive level of associations, you need to dot-qualify the names. e.g., A, B, A.C, A.D, A.C.E // + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + private boolean includeAssociations = false; + private Collection associationNamesToInclude = null; + + /******************************************************************************* ** @@ -229,4 +239,66 @@ public class GetInput extends AbstractTableActionInput return (this); } + + + /******************************************************************************* + ** Getter for includeAssociations + *******************************************************************************/ + public boolean getIncludeAssociations() + { + return (this.includeAssociations); + } + + + + /******************************************************************************* + ** Setter for includeAssociations + *******************************************************************************/ + public void setIncludeAssociations(boolean includeAssociations) + { + this.includeAssociations = includeAssociations; + } + + + + /******************************************************************************* + ** Fluent setter for includeAssociations + *******************************************************************************/ + public GetInput withIncludeAssociations(boolean includeAssociations) + { + this.includeAssociations = includeAssociations; + return (this); + } + + + + /******************************************************************************* + ** Getter for associationNamesToInclude + *******************************************************************************/ + public Collection getAssociationNamesToInclude() + { + return (this.associationNamesToInclude); + } + + + + /******************************************************************************* + ** Setter for associationNamesToInclude + *******************************************************************************/ + public void setAssociationNamesToInclude(Collection associationNamesToInclude) + { + this.associationNamesToInclude = associationNamesToInclude; + } + + + + /******************************************************************************* + ** Fluent setter for associationNamesToInclude + *******************************************************************************/ + public GetInput withAssociationNamesToInclude(Collection associationNamesToInclude) + { + this.associationNamesToInclude = associationNamesToInclude; + return (this); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryInput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryInput.java index c95fc9db..3a49001e 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryInput.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryInput.java @@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.query; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.Set; import com.kingsrook.qqq.backend.core.actions.QBackendTransaction; @@ -56,6 +57,14 @@ public class QueryInput extends AbstractTableActionInput private List queryJoins = null; + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // if you say you want to includeAssociations, you can limit which ones by passing them in associationNamesToInclude. // + // if you leave it null, you get all associations defined on the table. if you pass it as empty, you get none. // + // to go to a recursive level of associations, you need to dot-qualify the names. e.g., A, B, A.C, A.D, A.C.E // + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + private boolean includeAssociations = false; + private Collection associationNamesToInclude = null; + /******************************************************************************* @@ -425,4 +434,67 @@ public class QueryInput extends AbstractTableActionInput super.withTableName(tableName); return (this); } + + + + /******************************************************************************* + ** Getter for includeAssociations + *******************************************************************************/ + public boolean getIncludeAssociations() + { + return (this.includeAssociations); + } + + + + /******************************************************************************* + ** Setter for includeAssociations + *******************************************************************************/ + public void setIncludeAssociations(boolean includeAssociations) + { + this.includeAssociations = includeAssociations; + } + + + + /******************************************************************************* + ** Fluent setter for includeAssociations + *******************************************************************************/ + public QueryInput withIncludeAssociations(boolean includeAssociations) + { + this.includeAssociations = includeAssociations; + return (this); + } + + + + /******************************************************************************* + ** Getter for associationNamesToInclude + *******************************************************************************/ + public Collection getAssociationNamesToInclude() + { + return (this.associationNamesToInclude); + } + + + + /******************************************************************************* + ** Setter for associationNamesToInclude + *******************************************************************************/ + public void setAssociationNamesToInclude(Collection associationNamesToInclude) + { + this.associationNamesToInclude = associationNamesToInclude; + } + + + + /******************************************************************************* + ** Fluent setter for associationNamesToInclude + *******************************************************************************/ + public QueryInput withAssociationNamesToInclude(Collection associationNamesToInclude) + { + this.associationNamesToInclude = associationNamesToInclude; + return (this); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java index 94cbcc59..16be9792 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java @@ -170,6 +170,10 @@ public class MemoryRecordStore } else { + ////////////////////////////////////////////////////////////////////////////////// + // make sure we're not giving back records that are all full of associations... // + ////////////////////////////////////////////////////////////////////////////////// + qRecord.setAssociatedRecords(new HashMap<>()); records.add(qRecord); } } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/QueryActionTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/QueryActionTest.java index f33d02b0..b3be058f 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/QueryActionTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/QueryActionTest.java @@ -22,18 +22,25 @@ package com.kingsrook.qqq.backend.core.actions.tables; +import java.util.ArrayList; import java.util.List; import com.kingsrook.qqq.backend.core.BaseTest; 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; +import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput; import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.TestUtils; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; /******************************************************************************* @@ -124,4 +131,193 @@ class QueryActionTest extends BaseTest assertThat(records).isNotEmpty(); } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testQueryAssociations() throws QException + { + QContext.getQSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_TYPE_STORE_ALL_ACCESS, true); + insert2OrdersWith3Lines3LineExtrinsicsAnd4OrderExtrinsicAssociations(); + + QueryInput queryInput = new QueryInput(); + queryInput.setTableName(TestUtils.TABLE_NAME_ORDER); + queryInput.setFilter(new QQueryFilter()); + queryInput.setIncludeAssociations(true); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + + QRecord order0 = queryOutput.getRecords().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 = queryOutput.getRecords().get(1); + assertEquals(1, order1.getAssociatedRecords().get("orderLine").size()); + assertEquals(1, order1.getAssociatedRecords().get("extrinsics").size()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testQueryAssociationsNoAssociationNamesToInclude() throws QException + { + QContext.getQSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_TYPE_STORE_ALL_ACCESS, true); + insert2OrdersWith3Lines3LineExtrinsicsAnd4OrderExtrinsicAssociations(); + + QueryInput queryInput = new QueryInput(); + queryInput.setTableName(TestUtils.TABLE_NAME_ORDER); + queryInput.setFilter(new QQueryFilter()); + queryInput.setIncludeAssociations(true); + queryInput.setAssociationNamesToInclude(new ArrayList<>()); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + + QRecord order0 = queryOutput.getRecords().get(0); + assertTrue(CollectionUtils.nullSafeIsEmpty(order0.getAssociatedRecords())); + QRecord order1 = queryOutput.getRecords().get(1); + assertTrue(CollectionUtils.nullSafeIsEmpty(order1.getAssociatedRecords())); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testQueryAssociationsLimitedAssociationNamesToInclude() throws QException + { + QContext.getQSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_TYPE_STORE_ALL_ACCESS, true); + insert2OrdersWith3Lines3LineExtrinsicsAnd4OrderExtrinsicAssociations(); + + QueryInput queryInput = new QueryInput(); + queryInput.setTableName(TestUtils.TABLE_NAME_ORDER); + queryInput.setFilter(new QQueryFilter()); + queryInput.setIncludeAssociations(true); + queryInput.setAssociationNamesToInclude(List.of("orderLine")); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + + QRecord order0 = queryOutput.getRecords().get(0); + assertEquals(2, order0.getAssociatedRecords().get("orderLine").size()); + assertTrue(CollectionUtils.nullSafeIsEmpty(CollectionUtils.nonNullCollection(order0.getAssociatedRecords().get("extrinsics")))); + + QRecord orderLine00 = order0.getAssociatedRecords().get("orderLine").get(0); + assertTrue(CollectionUtils.nullSafeIsEmpty(CollectionUtils.nonNullCollection(orderLine00.getAssociatedRecords().get("extrinsics")))); + QRecord orderLine01 = order0.getAssociatedRecords().get("orderLine").get(1); + assertTrue(CollectionUtils.nullSafeIsEmpty(CollectionUtils.nonNullCollection(orderLine01.getAssociatedRecords().get("extrinsics")))); + + QRecord order1 = queryOutput.getRecords().get(1); + assertEquals(1, order1.getAssociatedRecords().get("orderLine").size()); + assertTrue(CollectionUtils.nullSafeIsEmpty(CollectionUtils.nonNullCollection(order1.getAssociatedRecords().get("extrinsics")))); + + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testQueryAssociationsLimitedAssociationNamesToIncludeChildTableDuplicatedAssociationNameExcluded() throws QException + { + QContext.getQSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_TYPE_STORE_ALL_ACCESS, true); + insert2OrdersWith3Lines3LineExtrinsicsAnd4OrderExtrinsicAssociations(); + + QueryInput queryInput = new QueryInput(); + queryInput.setTableName(TestUtils.TABLE_NAME_ORDER); + queryInput.setFilter(new QQueryFilter()); + queryInput.setIncludeAssociations(true); + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // say that we want extrinsics - but that should only get them from the top-level -- to get them from the child, we need orderLine.extrinsics // + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + queryInput.setAssociationNamesToInclude(List.of("orderLine", "extrinsics")); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + + QRecord order0 = queryOutput.getRecords().get(0); + assertEquals(2, order0.getAssociatedRecords().get("orderLine").size()); + assertEquals(3, order0.getAssociatedRecords().get("extrinsics").size()); + + QRecord orderLine00 = order0.getAssociatedRecords().get("orderLine").get(0); + assertTrue(CollectionUtils.nullSafeIsEmpty(CollectionUtils.nonNullCollection(orderLine00.getAssociatedRecords().get("extrinsics")))); + QRecord orderLine01 = order0.getAssociatedRecords().get("orderLine").get(1); + assertTrue(CollectionUtils.nullSafeIsEmpty(CollectionUtils.nonNullCollection(orderLine01.getAssociatedRecords().get("extrinsics")))); + + QRecord order1 = queryOutput.getRecords().get(1); + assertEquals(1, order1.getAssociatedRecords().get("orderLine").size()); + assertEquals(1, order1.getAssociatedRecords().get("extrinsics").size()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testQueryAssociationsLimitedAssociationNamesToIncludeChildTableDuplicatedAssociationNameIncluded() throws QException + { + QContext.getQSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_TYPE_STORE_ALL_ACCESS, true); + insert2OrdersWith3Lines3LineExtrinsicsAnd4OrderExtrinsicAssociations(); + + QueryInput queryInput = new QueryInput(); + queryInput.setTableName(TestUtils.TABLE_NAME_ORDER); + queryInput.setFilter(new QQueryFilter()); + queryInput.setIncludeAssociations(true); + + ///////////////////////////////////////////////////////////////////////////// + // this time say we want the orderLine.extrinsics - not the top-level ones // + ///////////////////////////////////////////////////////////////////////////// + queryInput.setAssociationNamesToInclude(List.of("orderLine", "orderLine.extrinsics")); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + + QRecord order0 = queryOutput.getRecords().get(0); + assertEquals(2, order0.getAssociatedRecords().get("orderLine").size()); + assertTrue(CollectionUtils.nullSafeIsEmpty(CollectionUtils.nonNullCollection(order0.getAssociatedRecords().get("extrinsics")))); + + QRecord orderLine00 = order0.getAssociatedRecords().get("orderLine").get(0); + assertFalse(CollectionUtils.nullSafeIsEmpty(CollectionUtils.nonNullCollection(orderLine00.getAssociatedRecords().get("extrinsics")))); + QRecord orderLine01 = order0.getAssociatedRecords().get("orderLine").get(1); + assertFalse(CollectionUtils.nullSafeIsEmpty(CollectionUtils.nonNullCollection(orderLine01.getAssociatedRecords().get("extrinsics")))); + + QRecord order1 = queryOutput.getRecords().get(1); + assertEquals(1, order1.getAssociatedRecords().get("orderLine").size()); + assertTrue(CollectionUtils.nullSafeIsEmpty(CollectionUtils.nonNullCollection(order1.getAssociatedRecords().get("extrinsics")))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static void insert2OrdersWith3Lines3LineExtrinsicsAnd4OrderExtrinsicAssociations() throws QException + { + InsertInput insertInput = new InsertInput(); + insertInput.setTableName(TestUtils.TABLE_NAME_ORDER); + insertInput.setRecords(List.of( + new QRecord().withValue("storeId", 1).withValue("orderNo", "ORD123") + + .withAssociatedRecord("orderLine", new QRecord().withValue("sku", "BASIC1").withValue("quantity", 1) + .withAssociatedRecord("extrinsics", new QRecord().withValue("key", "LINE-EXT-1.1").withValue("value", "LINE-VAL-1"))) + + .withAssociatedRecord("orderLine", new QRecord().withValue("sku", "BASIC2").withValue("quantity", 2) + .withAssociatedRecord("extrinsics", new QRecord().withValue("key", "LINE-EXT-2.1").withValue("value", "LINE-VAL-2")) + .withAssociatedRecord("extrinsics", new QRecord().withValue("key", "LINE-EXT-2.2").withValue("value", "LINE-VAL-3"))) + + .withAssociatedRecord("extrinsics", new QRecord().withValue("key", "MY-FIELD-1").withValue("value", "MY-VALUE-1")) + .withAssociatedRecord("extrinsics", new QRecord().withValue("key", "MY-FIELD-2").withValue("value", "MY-VALUE-2")) + .withAssociatedRecord("extrinsics", new QRecord().withValue("key", "MY-FIELD-3").withValue("value", "MY-VALUE-3")), + + new QRecord().withValue("storeId", 1).withValue("orderNo", "ORD124") + .withAssociatedRecord("orderLine", new QRecord().withValue("sku", "BASIC3").withValue("quantity", 3)) + .withAssociatedRecord("extrinsics", new QRecord().withValue("key", "YOUR-FIELD-1").withValue("value", "YOUR-VALUE-1")) + )); + new InsertAction().execute(insertInput); + } } diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/QRecordApiAdapter.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/QRecordApiAdapter.java index d72e9f72..381ce50d 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/QRecordApiAdapter.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/QRecordApiAdapter.java @@ -70,8 +70,6 @@ public class QRecordApiAdapter { ApiFieldMetaData apiFieldMetaData = ApiFieldMetaData.of(field); - // todo - what about display values / possible values? - String apiFieldName = ApiFieldMetaData.getEffectiveApiFieldName(field); if(StringUtils.hasContent(apiFieldMetaData.getReplacedByFieldName())) { @@ -82,6 +80,23 @@ public class QRecordApiAdapter outputRecord.put(apiFieldName, record.getValue(field.getName())); } } + + ////////////////////////////////////////////////////////////////////////////////////////////////// + // todo - should probably define in meta-data if an association is included in the api or not!! // + // and what its name is too... // + ////////////////////////////////////////////////////////////////////////////////////////////////// + QTableMetaData table = QContext.getQInstance().getTable(tableName); + for(Association association : CollectionUtils.nonNullList(table.getAssociations())) + { + ArrayList> associationList = new ArrayList<>(); + outputRecord.put(association.getName(), associationList); + + for(QRecord associatedRecord : CollectionUtils.nonNullList(CollectionUtils.nonNullMap(record.getAssociatedRecords()).get(association.getName()))) + { + associationList.add(qRecordToApiMap(associatedRecord, association.getAssociatedTableName(), apiVersion)); + } + } + return (outputRecord); } diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandler.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandler.java index beb23468..b26992d3 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandler.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandler.java @@ -567,6 +567,7 @@ public class QJavalinApiHandler // and throw a 400-series error (tell the user bad-request), rather than, we're doing a 500 (server error) getInput.setPrimaryKey(primaryKey); + getInput.setIncludeAssociations(true); GetAction getAction = new GetAction(); GetOutput getOutput = getAction.execute(getInput); @@ -616,6 +617,7 @@ public class QJavalinApiHandler QJavalinAccessLogger.logStart("apiQuery", logPair("table", tableName)); queryInput.setTableName(tableName); + queryInput.setIncludeAssociations(true); PermissionsHelper.checkTablePermissionThrowing(queryInput, TablePermissionSubType.READ); @@ -795,6 +797,16 @@ public class QJavalinApiHandler output.put("pageNo", pageNo); output.put("pageSize", pageSize); + //////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // map record fields for api // + // note - don't put them in the output until after the count, just because that looks a little nicer, i think // + //////////////////////////////////////////////////////////////////////////////////////////////////////////////// + ArrayList> records = new ArrayList<>(); + for(QRecord record : queryOutput.getRecords()) + { + records.add(QRecordApiAdapter.qRecordToApiMap(record, tableName, version)); + } + ///////////////////////////// // optionally do the count // ///////////////////////////// @@ -807,14 +819,6 @@ public class QJavalinApiHandler output.put("count", countOutput.getCount()); } - /////////////////////////////// - // map record fields for api // - /////////////////////////////// - ArrayList> records = new ArrayList<>(); - for(QRecord record : queryOutput.getRecords()) - { - records.add(QRecordApiAdapter.qRecordToApiMap(record, tableName, version)); - } output.put("records", records); QJavalinAccessLogger.logEndSuccess(logPair("recordCount", queryOutput.getRecords().size()), QJavalinAccessLogger.logPairIfSlow("filter", filter, SLOW_LOG_THRESHOLD_MS)); diff --git a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandlerTest.java b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandlerTest.java index 20528df4..e5a7c434 100644 --- a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandlerTest.java +++ b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandlerTest.java @@ -195,6 +195,29 @@ class QJavalinApiHandlerTest extends BaseTest + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testGetAssociations() throws QException + { + insert1Order3Lines4LineExtrinsicsAnd1OrderExtrinsic(); + + HttpResponse response = Unirest.get(BASE_URL + "/api/" + VERSION + "/order/1").asString(); + assertEquals(HttpStatus.OK_200, response.getStatus()); + JSONObject jsonObject = new JSONObject(response.getBody()); + System.out.println(jsonObject.toString(3)); + JSONArray orderLines = jsonObject.getJSONArray("orderLines"); + assertEquals(3, orderLines.length()); + JSONObject orderLine0 = orderLines.getJSONObject(0); + JSONArray lineExtrinsics = orderLine0.getJSONArray("extrinsics"); + assertEquals(3, lineExtrinsics.length()); + assertEquals("Size", lineExtrinsics.getJSONObject(0).getString("key")); + assertEquals("Medium", lineExtrinsics.getJSONObject(0).getString("value")); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -406,6 +429,30 @@ class QJavalinApiHandlerTest extends BaseTest + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testQueryAssociations() throws QException + { + insert1Order3Lines4LineExtrinsicsAnd1OrderExtrinsic(); + + HttpResponse response = Unirest.get(BASE_URL + "/api/" + VERSION + "/order/query?id=1").asString(); + assertEquals(HttpStatus.OK_200, response.getStatus()); + JSONObject jsonObject = new JSONObject(response.getBody()); + System.out.println(jsonObject.toString(3)); + JSONObject order0 = jsonObject.getJSONArray("records").getJSONObject(0); + JSONArray orderLines = order0.getJSONArray("orderLines"); + assertEquals(3, orderLines.length()); + JSONObject orderLine0 = orderLines.getJSONObject(0); + JSONArray lineExtrinsics = orderLine0.getJSONArray("extrinsics"); + assertEquals(3, lineExtrinsics.length()); + assertEquals("Size", lineExtrinsics.getJSONObject(0).getString("key")); + assertEquals("Medium", lineExtrinsics.getJSONObject(0).getString("value")); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -958,19 +1005,7 @@ class QJavalinApiHandlerTest extends BaseTest @Test void testDeleteAssociations() throws QException { - InsertInput insertInput = new InsertInput(); - insertInput.setTableName(TestUtils.TABLE_NAME_ORDER); - insertInput.setRecords(List.of(new QRecord().withValue("id", 1).withValue("orderNo", "ORD123").withValue("storeId", 47) - .withAssociatedRecord("orderLines", new QRecord().withValue("lineNumber", 1).withValue("sku", "BASIC1").withValue("quantity", 42) - .withAssociatedRecord("extrinsics", new QRecord().withValue("key", "Size").withValue("value", "Medium")) - .withAssociatedRecord("extrinsics", new QRecord().withValue("key", "Discount").withValue("value", "3.50")) - .withAssociatedRecord("extrinsics", new QRecord().withValue("key", "Color").withValue("value", "Red"))) - .withAssociatedRecord("orderLines", new QRecord().withValue("lineNumber", 2).withValue("sku", "BASIC2").withValue("quantity", 42) - .withAssociatedRecord("extrinsics", new QRecord().withValue("key", "Size").withValue("value", "Medium"))) - .withAssociatedRecord("orderLines", new QRecord().withValue("lineNumber", 3).withValue("sku", "BASIC3").withValue("quantity", 42)) - .withAssociatedRecord("extrinsics", new QRecord().withValue("key", "shopifyOrderNo").withValue("value", "#1032")) - )); - new InsertAction().execute(insertInput); + insert1Order3Lines4LineExtrinsicsAnd1OrderExtrinsic(); assertEquals(1, queryTable(TestUtils.TABLE_NAME_ORDER).size()); assertEquals(4, queryTable(TestUtils.TABLE_NAME_LINE_ITEM_EXTRINSIC).size()); @@ -989,6 +1024,28 @@ class QJavalinApiHandlerTest extends BaseTest + /******************************************************************************* + ** + *******************************************************************************/ + private static void insert1Order3Lines4LineExtrinsicsAnd1OrderExtrinsic() throws QException + { + InsertInput insertInput = new InsertInput(); + insertInput.setTableName(TestUtils.TABLE_NAME_ORDER); + insertInput.setRecords(List.of(new QRecord().withValue("id", 1).withValue("orderNo", "ORD123").withValue("storeId", 47) + .withAssociatedRecord("orderLines", new QRecord().withValue("lineNumber", 1).withValue("sku", "BASIC1").withValue("quantity", 42) + .withAssociatedRecord("extrinsics", new QRecord().withValue("key", "Size").withValue("value", "Medium")) + .withAssociatedRecord("extrinsics", new QRecord().withValue("key", "Discount").withValue("value", "3.50")) + .withAssociatedRecord("extrinsics", new QRecord().withValue("key", "Color").withValue("value", "Red"))) + .withAssociatedRecord("orderLines", new QRecord().withValue("lineNumber", 2).withValue("sku", "BASIC2").withValue("quantity", 42) + .withAssociatedRecord("extrinsics", new QRecord().withValue("key", "Size").withValue("value", "Medium"))) + .withAssociatedRecord("orderLines", new QRecord().withValue("lineNumber", 3).withValue("sku", "BASIC3").withValue("quantity", 42)) + .withAssociatedRecord("extrinsics", new QRecord().withValue("key", "shopifyOrderNo").withValue("value", "#1032")) + )); + new InsertAction().execute(insertInput); + } + + + /******************************************************************************* ** *******************************************************************************/