Adding associated records to Get, Query.

This commit is contained in:
2023-03-30 16:56:18 -05:00
parent 3df4513cd1
commit 7e368c6ff9
9 changed files with 576 additions and 23 deletions

View File

@ -329,6 +329,8 @@ public class GetAction
} }
queryInput.setFilter(filter); queryInput.setFilter(filter);
queryInput.setIncludeAssociations(getInput.getIncludeAssociations());
queryInput.setAssociationNamesToInclude(getInput.getAssociationNamesToInclude());
queryInput.setShouldFetchHeavyFields(getInput.getShouldFetchHeavyFields()); queryInput.setShouldFetchHeavyFields(getInput.getShouldFetchHeavyFields());
QueryOutput queryOutput = new QueryAction().execute(queryInput); QueryOutput queryOutput = new QueryAction().execute(queryInput);

View File

@ -22,8 +22,13 @@
package com.kingsrook.qqq.backend.core.actions.tables; 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.List;
import java.util.Optional; import java.util.Optional;
import java.util.Set;
import com.kingsrook.qqq.backend.core.actions.ActionHelper; 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.AbstractPostQueryCustomizer;
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader; 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.reporting.BufferedRecordPipe;
import com.kingsrook.qqq.backend.core.actions.values.QPossibleValueTranslator; import com.kingsrook.qqq.backend.core.actions.values.QPossibleValueTranslator;
import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter; 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.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger; 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.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput; 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.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.QBackendModuleDispatcher;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface; 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); 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(); QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher();
QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(queryInput.getBackend()); QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(queryInput.getBackend());
// todo pre-customization - just get to modify the request? // todo pre-customization - just get to modify the request?
@ -86,11 +109,119 @@ public class QueryAction
postRecordActions(queryOutput.getRecords()); postRecordActions(queryOutput.getRecords());
} }
if(queryInput.getIncludeAssociations())
{
manageAssociations(queryInput, queryOutput);
}
return 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<List<Serializable>, QRecord> outerResultMap = new ListingHash<>();
if(join.getJoinOns().size() == 1)
{
JoinOn joinOn = join.getJoinOns().get(0);
Set<Serializable> 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<Serializable> 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<Serializable> 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<String> buildNextLevelAssociationNamesToInclude(String name, Collection<String> associationNamesToInclude)
{
if(associationNamesToInclude == null)
{
return (associationNamesToInclude);
}
Set<String> 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., ** 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, ** not one created via List.of()). This may include setting display values,

View File

@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.get;
import java.io.Serializable; import java.io.Serializable;
import java.util.Collection;
import java.util.Map; import java.util.Map;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction; import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput; import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
@ -44,6 +45,15 @@ public class GetInput extends AbstractTableActionInput
private boolean shouldFetchHeavyFields = true; 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<String> associationNamesToInclude = null;
/******************************************************************************* /*******************************************************************************
** **
@ -229,4 +239,66 @@ public class GetInput extends AbstractTableActionInput
return (this); 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<String> getAssociationNamesToInclude()
{
return (this.associationNamesToInclude);
}
/*******************************************************************************
** Setter for associationNamesToInclude
*******************************************************************************/
public void setAssociationNamesToInclude(Collection<String> associationNamesToInclude)
{
this.associationNamesToInclude = associationNamesToInclude;
}
/*******************************************************************************
** Fluent setter for associationNamesToInclude
*******************************************************************************/
public GetInput withAssociationNamesToInclude(Collection<String> associationNamesToInclude)
{
this.associationNamesToInclude = associationNamesToInclude;
return (this);
}
} }

View File

@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.query;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction; import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
@ -56,6 +57,14 @@ public class QueryInput extends AbstractTableActionInput
private List<QueryJoin> queryJoins = null; private List<QueryJoin> 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<String> associationNamesToInclude = null;
/******************************************************************************* /*******************************************************************************
@ -425,4 +434,67 @@ public class QueryInput extends AbstractTableActionInput
super.withTableName(tableName); super.withTableName(tableName);
return (this); 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<String> getAssociationNamesToInclude()
{
return (this.associationNamesToInclude);
}
/*******************************************************************************
** Setter for associationNamesToInclude
*******************************************************************************/
public void setAssociationNamesToInclude(Collection<String> associationNamesToInclude)
{
this.associationNamesToInclude = associationNamesToInclude;
}
/*******************************************************************************
** Fluent setter for associationNamesToInclude
*******************************************************************************/
public QueryInput withAssociationNamesToInclude(Collection<String> associationNamesToInclude)
{
this.associationNamesToInclude = associationNamesToInclude;
return (this);
}
} }

View File

@ -170,6 +170,10 @@ public class MemoryRecordStore
} }
else else
{ {
//////////////////////////////////////////////////////////////////////////////////
// make sure we're not giving back records that are all full of associations... //
//////////////////////////////////////////////////////////////////////////////////
qRecord.setAssociatedRecords(new HashMap<>());
records.add(qRecord); records.add(qRecord);
} }
} }

View File

@ -22,18 +22,25 @@
package com.kingsrook.qqq.backend.core.actions.tables; package com.kingsrook.qqq.backend.core.actions.tables;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import com.kingsrook.qqq.backend.core.BaseTest; import com.kingsrook.qqq.backend.core.BaseTest;
import com.kingsrook.qqq.backend.core.actions.reporting.RecordPipe; import com.kingsrook.qqq.backend.core.actions.reporting.RecordPipe;
import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException; 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.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput; 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.data.QRecord;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.TestUtils; import com.kingsrook.qqq.backend.core.utils.TestUtils;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat; 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.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
/******************************************************************************* /*******************************************************************************
@ -124,4 +131,193 @@ class QueryActionTest extends BaseTest
assertThat(records).isNotEmpty(); 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);
}
} }

View File

@ -70,8 +70,6 @@ public class QRecordApiAdapter
{ {
ApiFieldMetaData apiFieldMetaData = ApiFieldMetaData.of(field); ApiFieldMetaData apiFieldMetaData = ApiFieldMetaData.of(field);
// todo - what about display values / possible values?
String apiFieldName = ApiFieldMetaData.getEffectiveApiFieldName(field); String apiFieldName = ApiFieldMetaData.getEffectiveApiFieldName(field);
if(StringUtils.hasContent(apiFieldMetaData.getReplacedByFieldName())) if(StringUtils.hasContent(apiFieldMetaData.getReplacedByFieldName()))
{ {
@ -82,6 +80,23 @@ public class QRecordApiAdapter
outputRecord.put(apiFieldName, record.getValue(field.getName())); 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<Map<String, Serializable>> 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); return (outputRecord);
} }

View File

@ -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) // and throw a 400-series error (tell the user bad-request), rather than, we're doing a 500 (server error)
getInput.setPrimaryKey(primaryKey); getInput.setPrimaryKey(primaryKey);
getInput.setIncludeAssociations(true);
GetAction getAction = new GetAction(); GetAction getAction = new GetAction();
GetOutput getOutput = getAction.execute(getInput); GetOutput getOutput = getAction.execute(getInput);
@ -616,6 +617,7 @@ public class QJavalinApiHandler
QJavalinAccessLogger.logStart("apiQuery", logPair("table", tableName)); QJavalinAccessLogger.logStart("apiQuery", logPair("table", tableName));
queryInput.setTableName(tableName); queryInput.setTableName(tableName);
queryInput.setIncludeAssociations(true);
PermissionsHelper.checkTablePermissionThrowing(queryInput, TablePermissionSubType.READ); PermissionsHelper.checkTablePermissionThrowing(queryInput, TablePermissionSubType.READ);
@ -795,6 +797,16 @@ public class QJavalinApiHandler
output.put("pageNo", pageNo); output.put("pageNo", pageNo);
output.put("pageSize", pageSize); 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<Map<String, Serializable>> records = new ArrayList<>();
for(QRecord record : queryOutput.getRecords())
{
records.add(QRecordApiAdapter.qRecordToApiMap(record, tableName, version));
}
///////////////////////////// /////////////////////////////
// optionally do the count // // optionally do the count //
///////////////////////////// /////////////////////////////
@ -807,14 +819,6 @@ public class QJavalinApiHandler
output.put("count", countOutput.getCount()); output.put("count", countOutput.getCount());
} }
///////////////////////////////
// map record fields for api //
///////////////////////////////
ArrayList<Map<String, Serializable>> records = new ArrayList<>();
for(QRecord record : queryOutput.getRecords())
{
records.add(QRecordApiAdapter.qRecordToApiMap(record, tableName, version));
}
output.put("records", records); output.put("records", records);
QJavalinAccessLogger.logEndSuccess(logPair("recordCount", queryOutput.getRecords().size()), QJavalinAccessLogger.logPairIfSlow("filter", filter, SLOW_LOG_THRESHOLD_MS)); QJavalinAccessLogger.logEndSuccess(logPair("recordCount", queryOutput.getRecords().size()), QJavalinAccessLogger.logPairIfSlow("filter", filter, SLOW_LOG_THRESHOLD_MS));

View File

@ -195,6 +195,29 @@ class QJavalinApiHandlerTest extends BaseTest
/*******************************************************************************
**
*******************************************************************************/
@Test
void testGetAssociations() throws QException
{
insert1Order3Lines4LineExtrinsicsAnd1OrderExtrinsic();
HttpResponse<String> 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<String> 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 @Test
void testDeleteAssociations() throws QException void testDeleteAssociations() throws QException
{ {
InsertInput insertInput = new InsertInput(); insert1Order3Lines4LineExtrinsicsAnd1OrderExtrinsic();
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);
assertEquals(1, queryTable(TestUtils.TABLE_NAME_ORDER).size()); assertEquals(1, queryTable(TestUtils.TABLE_NAME_ORDER).size());
assertEquals(4, queryTable(TestUtils.TABLE_NAME_LINE_ITEM_EXTRINSIC).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);
}
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/