Initial support for associated records (implemented insert, delete).

Include "api" on audit.
This commit is contained in:
2023-03-27 09:52:39 -05:00
parent 17d4c81cc3
commit ba805a4c92
15 changed files with 1052 additions and 85 deletions

View File

@ -54,6 +54,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
@ -106,6 +107,13 @@ public class DMLAuditAction extends AbstractQActionFunction<DMLAuditInput, DMLAu
}
}
QSession qSession = QContext.getQSession();
String apiVersion = qSession.getValue("apiVersion");
if(apiVersion != null)
{
contextSuffix += (" via API Version: " + apiVersion);
}
AuditInput auditInput = new AuditInput();
if(auditLevel.equals(AuditLevel.RECORD) || (auditLevel.equals(AuditLevel.FIELD) && !dmlType.supportsFields))
{
@ -125,7 +133,7 @@ public class DMLAuditAction extends AbstractQActionFunction<DMLAuditInput, DMLAu
///////////////////////////////////////////////////////////////////
// do many audits, all with field level details, for FIELD level //
///////////////////////////////////////////////////////////////////
QPossibleValueTranslator qPossibleValueTranslator = new QPossibleValueTranslator(QContext.getQInstance(), QContext.getQSession());
QPossibleValueTranslator qPossibleValueTranslator = new QPossibleValueTranslator(QContext.getQInstance(), qSession);
qPossibleValueTranslator.translatePossibleValuesInRecords(table, CollectionUtils.mergeLists(recordList, oldRecordList));
//////////////////////////////////////////

View File

@ -28,6 +28,7 @@ import java.util.List;
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
import com.kingsrook.qqq.backend.core.actions.audits.DMLAuditAction;
import com.kingsrook.qqq.backend.core.actions.interfaces.DeleteInterface;
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.audits.DMLAuditInput;
@ -40,6 +41,9 @@ 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.audits.AuditLevel;
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;
@ -91,6 +95,9 @@ public class DeleteAction
List<QRecord> recordListForAudit = getRecordListForAuditIfNeeded(deleteInput);
DeleteOutput deleteOutput = deleteInterface.execute(deleteInput);
manageAssociations(deleteInput);
// todo post-customization - can do whatever w/ the result if you want
new DMLAuditAction().execute(new DMLAuditInput().withTableActionInput(deleteInput).withRecordList(recordListForAudit));
@ -100,6 +107,46 @@ public class DeleteAction
/*******************************************************************************
**
*******************************************************************************/
private void manageAssociations(DeleteInput deleteInput) throws QException
{
QTableMetaData table = deleteInput.getTable();
for(Association association : CollectionUtils.nonNullList(table.getAssociations()))
{
// 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()))
QQueryFilter filter = new QQueryFilter();
if(join.getJoinOns().size() == 1 && join.getJoinOns().get(0).getLeftField().equals(table.getPrimaryKeyField()))
{
filter.addCriteria(new QFilterCriteria(join.getJoinOns().get(0).getRightField(), QCriteriaOperator.IN, deleteInput.getPrimaryKeys()));
}
else
{
throw (new QException("Join of this type is not supported for an associated delete at this time..."));
}
QTableMetaData associatedTable = QContext.getQInstance().getTable(association.getAssociatedTableName());
QueryInput queryInput = new QueryInput();
queryInput.setTableName(association.getAssociatedTableName());
queryInput.setFilter(filter);
QueryOutput queryOutput = new QueryAction().execute(queryInput);
List<Serializable> associatedKeys = queryOutput.getRecords().stream().map(r -> r.getValue(associatedTable.getPrimaryKeyField())).toList();
DeleteInput nextLevelDeleteInput = new DeleteInput();
nextLevelDeleteInput.setTableName(association.getAssociatedTableName());
nextLevelDeleteInput.setPrimaryKeys(associatedKeys);
DeleteOutput nextLevelDeleteOutput = new DeleteAction().execute(nextLevelDeleteInput);
}
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.actions.tables;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@ -40,12 +41,16 @@ import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
import com.kingsrook.qqq.backend.core.actions.tables.helpers.UniqueKeyHelper;
import com.kingsrook.qqq.backend.core.actions.values.ValueBehaviorApplier;
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.audits.DMLAuditInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput;
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.model.metadata.tables.UniqueKey;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher;
@ -75,15 +80,16 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
Optional<AbstractPostInsertCustomizer> postInsertCustomizer = QCodeLoader.getTableCustomizer(AbstractPostInsertCustomizer.class, table, TableCustomizers.POST_INSERT_RECORD.getRole());
setAutomationStatusField(insertInput);
ValueBehaviorApplier.applyFieldBehaviors(insertInput.getInstance(), table, insertInput.getRecords());
// todo - need to handle records with errors coming out of here...
QBackendModuleInterface qModule = getBackendModuleInterface(insertInput);
// todo pre-customization - just get to modify the request?
ValueBehaviorApplier.applyFieldBehaviors(insertInput.getInstance(), table, insertInput.getRecords());
setErrorsIfUniqueKeyErrors(insertInput, table);
InsertOutput insertOutput = qModule.getInsertInterface().execute(insertInput);
manageAssociations(table, insertOutput.getRecords());
// todo post-customization - can do whatever w/ the result if you want
new DMLAuditAction().execute(new DMLAuditInput().withTableActionInput(insertInput).withRecordList(insertOutput.getRecords()));
@ -98,6 +104,43 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
}
/*******************************************************************************
**
*******************************************************************************/
private void manageAssociations(QTableMetaData table, List<QRecord> insertedRecords) throws QException
{
for(Association association : CollectionUtils.nonNullList(table.getAssociations()))
{
// 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()))
List<QRecord> nextLevelInserts = new ArrayList<>();
for(QRecord record : insertedRecords)
{
if(record.getAssociatedRecords() != null && record.getAssociatedRecords().containsKey(association.getName()))
{
for(QRecord associatedRecord : CollectionUtils.nonNullList(record.getAssociatedRecords().get(association.getName())))
{
for(JoinOn joinOn : join.getJoinOns())
{
associatedRecord.setValue(joinOn.getRightField(), record.getValue(joinOn.getLeftField()));
}
nextLevelInserts.add(associatedRecord);
}
}
}
InsertInput nextLevelInsertInput = new InsertInput();
nextLevelInsertInput.setTableName(association.getAssociatedTableName());
nextLevelInsertInput.setRecords(nextLevelInserts);
InsertOutput nextLevelInsertOutput = new InsertAction().execute(nextLevelInsertInput);
}
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -28,6 +28,7 @@ import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@ -65,6 +66,8 @@ public class QRecord implements Serializable
private Map<String, Serializable> backendDetails = new LinkedHashMap<>();
private List<String> errors = new ArrayList<>();
private Map<String, List<QRecord>> associatedRecords = new HashMap<>();
public static final String BACKEND_DETAILS_TYPE_JSON_SOURCE_OBJECT = "jsonSourceObject";
@ -102,6 +105,7 @@ public class QRecord implements Serializable
this.displayValues = doDeepCopy(record.displayValues);
this.backendDetails = doDeepCopy(record.backendDetails);
this.errors = doDeepCopy(record.errors);
this.associatedRecords = doDeepCopy(record.associatedRecords);
}
@ -575,4 +579,66 @@ public class QRecord implements Serializable
return (QRecordEntity.fromQRecord(c, this));
}
/*******************************************************************************
** Getter for associatedRecords
*******************************************************************************/
public Map<String, List<QRecord>> getAssociatedRecords()
{
return (this.associatedRecords);
}
/*******************************************************************************
** Setter for associatedRecords
*******************************************************************************/
public void setAssociatedRecords(Map<String, List<QRecord>> associatedRecords)
{
this.associatedRecords = associatedRecords;
}
/*******************************************************************************
** Fluent setter for associatedRecords
*******************************************************************************/
public QRecord withAssociatedRecords(Map<String, List<QRecord>> associatedRecords)
{
this.associatedRecords = associatedRecords;
return (this);
}
/*******************************************************************************
** Fluent setter for associatedRecords
*******************************************************************************/
public QRecord withAssociatedRecords(String name, List<QRecord> associatedRecords)
{
if(this.associatedRecords == null)
{
this.associatedRecords = new HashMap<>();
}
this.associatedRecords.put(name, associatedRecords);
return (this);
}
/*******************************************************************************
** Fluent setter for associatedRecord
*******************************************************************************/
public QRecord withAssociatedRecord(String name, QRecord associatedRecord)
{
if(this.associatedRecords == null)
{
this.associatedRecords = new HashMap<>();
}
this.associatedRecords.putIfAbsent(name, new ArrayList<>());
this.associatedRecords.get(name).add(associatedRecord);
return (this);
}
}

View File

@ -0,0 +1,128 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.metadata.tables;
/*******************************************************************************
** definition of a qqq table that is "associated" with another table, e.g.,
** managed along with it - such as child-records under a parent record.
*******************************************************************************/
public class Association
{
private String name;
private String associatedTableName;
private String joinName;
/*******************************************************************************
** Getter for name
*******************************************************************************/
public String getName()
{
return (this.name);
}
/*******************************************************************************
** Setter for name
*******************************************************************************/
public void setName(String name)
{
this.name = name;
}
/*******************************************************************************
** Fluent setter for name
*******************************************************************************/
public Association withName(String name)
{
this.name = name;
return (this);
}
/*******************************************************************************
** Getter for associatedTableName
*******************************************************************************/
public String getAssociatedTableName()
{
return (this.associatedTableName);
}
/*******************************************************************************
** Setter for associatedTableName
*******************************************************************************/
public void setAssociatedTableName(String associatedTableName)
{
this.associatedTableName = associatedTableName;
}
/*******************************************************************************
** Fluent setter for associatedTableName
*******************************************************************************/
public Association withAssociatedTableName(String associatedTableName)
{
this.associatedTableName = associatedTableName;
return (this);
}
/*******************************************************************************
** Getter for joinName
*******************************************************************************/
public String getJoinName()
{
return (this.joinName);
}
/*******************************************************************************
** Setter for joinName
*******************************************************************************/
public void setJoinName(String joinName)
{
this.joinName = joinName;
}
/*******************************************************************************
** Fluent setter for joinName
*******************************************************************************/
public Association withJoinName(String joinName)
{
this.joinName = joinName;
return (this);
}
}

View File

@ -72,6 +72,7 @@ public class QTableMetaData implements QAppChildMetaData, Serializable, MetaData
private Map<String, QFieldMetaData> fields;
private List<UniqueKey> uniqueKeys;
private List<Association> associations;
private List<RecordSecurityLock> recordSecurityLocks;
private QPermissionRules permissionRules;
@ -1237,4 +1238,50 @@ public class QTableMetaData implements QAppChildMetaData, Serializable, MetaData
return (this);
}
/*******************************************************************************
** Getter for associations
*******************************************************************************/
public List<Association> getAssociations()
{
return (this.associations);
}
/*******************************************************************************
** Setter for associations
*******************************************************************************/
public void setAssociations(List<Association> associations)
{
this.associations = associations;
}
/*******************************************************************************
** Fluent setter for associations
*******************************************************************************/
public QTableMetaData withAssociations(List<Association> associations)
{
this.associations = associations;
return (this);
}
/*******************************************************************************
** Fluent setter for associations
*******************************************************************************/
public QTableMetaData withAssociation(Association association)
{
if(this.associations == null)
{
this.associations = new ArrayList<>();
}
this.associations.add(association);
return (this);
}
}

View File

@ -31,7 +31,10 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
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.QFilterOrderBy;
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.audits.AuditsMetaDataProvider;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
@ -149,4 +152,145 @@ class DeleteActionTest extends BaseTest
assertTrue(audits.stream().allMatch(r -> r.getValueString("message").equals("Record was Deleted")));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testAssociatedDeletes() throws QException
{
{
InsertInput insertInput = new InsertInput();
insertInput.setTableName(TestUtils.TABLE_NAME_ORDER);
insertInput.setRecords(List.of(
new QRecord().withValue("id", 1),
new QRecord().withValue("id", 2),
new QRecord().withValue("id", 3)
));
new InsertAction().execute(insertInput);
}
{
InsertInput insertInput = new InsertInput();
insertInput.setTableName(TestUtils.TABLE_NAME_ORDER_EXTRINSIC);
insertInput.setRecords(List.of(
new QRecord().withValue("id", 1).withValue("orderId", 1),
new QRecord().withValue("id", 2).withValue("orderId", 1),
new QRecord().withValue("id", 3).withValue("orderId", 1),
new QRecord().withValue("id", 4).withValue("orderId", 1),
new QRecord().withValue("id", 5).withValue("orderId", 3),
new QRecord().withValue("id", 6).withValue("orderId", 3),
new QRecord().withValue("id", 7).withValue("orderId", 3)
));
new InsertAction().execute(insertInput);
}
{
InsertInput insertInput = new InsertInput();
insertInput.setTableName(TestUtils.TABLE_NAME_LINE_ITEM);
insertInput.setRecords(List.of(
new QRecord().withValue("id", 1).withValue("orderId", 1),
new QRecord().withValue("id", 2).withValue("orderId", 1),
new QRecord().withValue("id", 3).withValue("orderId", 2),
new QRecord().withValue("id", 4).withValue("orderId", 3),
new QRecord().withValue("id", 5).withValue("orderId", 3),
new QRecord().withValue("id", 6).withValue("orderId", 3)
));
new InsertAction().execute(insertInput);
}
{
InsertInput insertInput = new InsertInput();
insertInput.setTableName(TestUtils.TABLE_NAME_LINE_ITEM_EXTRINSIC);
insertInput.setRecords(List.of(
new QRecord().withValue("id", 1).withValue("lineItemId", 1), // orderId: 1
new QRecord().withValue("id", 2).withValue("lineItemId", 1), // orderId: 1
new QRecord().withValue("id", 3).withValue("lineItemId", 2), // orderId: 1
new QRecord().withValue("id", 4).withValue("lineItemId", 2), // orderId: 1
new QRecord().withValue("id", 5).withValue("lineItemId", 3), // orderId: 2
new QRecord().withValue("id", 6).withValue("lineItemId", 3), // orderId: 2
new QRecord().withValue("id", 7).withValue("lineItemId", 4), // orderId: 3
new QRecord().withValue("id", 8).withValue("lineItemId", 5), // orderId: 3
new QRecord().withValue("id", 9).withValue("lineItemId", 6) // orderId: 3
));
new InsertAction().execute(insertInput);
}
/////////////////////////////////////////////////////////
// assert about how many things we originally inserted //
/////////////////////////////////////////////////////////
assertEquals(3, queryTable(TestUtils.TABLE_NAME_ORDER).size());
assertEquals(7, queryTable(TestUtils.TABLE_NAME_ORDER_EXTRINSIC).size());
assertEquals(6, queryTable(TestUtils.TABLE_NAME_LINE_ITEM).size());
assertEquals(9, queryTable(TestUtils.TABLE_NAME_LINE_ITEM_EXTRINSIC).size());
////////////////////////////////
// delete (cascading) order 1 //
////////////////////////////////
DeleteInput deleteInput = new DeleteInput();
deleteInput.setTableName(TestUtils.TABLE_NAME_ORDER);
deleteInput.setPrimaryKeys(List.of(1));
new DeleteAction().execute(deleteInput);
//////////////////////////////////////////////////
// assert that the associated data were deleted //
//////////////////////////////////////////////////
assertEquals(2, queryTable(TestUtils.TABLE_NAME_ORDER).size());
assertEquals(3, queryTable(TestUtils.TABLE_NAME_ORDER_EXTRINSIC).size());
assertEquals(4, queryTable(TestUtils.TABLE_NAME_LINE_ITEM).size());
assertEquals(5, queryTable(TestUtils.TABLE_NAME_LINE_ITEM_EXTRINSIC).size());
////////////////////
// delete order 2 //
////////////////////
deleteInput.setPrimaryKeys(List.of(2));
new DeleteAction().execute(deleteInput);
//////////////////////////////////////////////////
// assert that the associated data were deleted //
//////////////////////////////////////////////////
assertEquals(1, queryTable(TestUtils.TABLE_NAME_ORDER).size());
assertEquals(3, queryTable(TestUtils.TABLE_NAME_ORDER_EXTRINSIC).size());
assertEquals(3, queryTable(TestUtils.TABLE_NAME_LINE_ITEM).size());
assertEquals(3, queryTable(TestUtils.TABLE_NAME_LINE_ITEM_EXTRINSIC).size());
////////////////////
// delete order 3 //
////////////////////
deleteInput.setPrimaryKeys(List.of(3));
new DeleteAction().execute(deleteInput);
///////////////////////////////
// everything is deleted now //
///////////////////////////////
assertEquals(0, queryTable(TestUtils.TABLE_NAME_ORDER).size());
assertEquals(0, queryTable(TestUtils.TABLE_NAME_ORDER_EXTRINSIC).size());
assertEquals(0, queryTable(TestUtils.TABLE_NAME_LINE_ITEM).size());
assertEquals(0, queryTable(TestUtils.TABLE_NAME_LINE_ITEM_EXTRINSIC).size());
////////////////////////////////////////////////
// make sure no errors if we try more deletes //
////////////////////////////////////////////////
deleteInput.setPrimaryKeys(List.of(3));
new DeleteAction().execute(deleteInput);
deleteInput.setPrimaryKeys(List.of(1, 2, 3, 4));
new DeleteAction().execute(deleteInput);
}
/*******************************************************************************
**
*******************************************************************************/
private static List<QRecord> queryTable(String tableName) throws QException
{
QueryInput queryInput = new QueryInput();
queryInput.setTableName(tableName);
queryInput.setFilter(new QQueryFilter().withOrderBy(new QFilterOrderBy("id")));
QueryOutput queryOutput = new QueryAction().execute(queryInput);
return (queryOutput.getRecords());
}
}

View File

@ -209,4 +209,78 @@ class InsertActionTest extends BaseTest
assertTrue(CollectionUtils.nullSafeIsEmpty(insertOutput.getRecords().get(1).getErrors()));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testInsertAssociations() throws QException
{
QInstance qInstance = QContext.getQInstance();
QContext.getQSession().withSecurityKeyValue("storeId", 1);
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("extrinsics", new QRecord().withValue("key", "YOUR-FIELD-1").withValue("value", "YOUR-VALUE-1"))
));
new InsertAction().execute(insertInput);
List<QRecord> orders = TestUtils.queryTable(qInstance, TestUtils.TABLE_NAME_ORDER);
assertEquals(2, orders.size());
assertEquals(1, orders.get(0).getValueInteger("id"));
assertEquals(2, orders.get(1).getValueInteger("id"));
assertEquals("ORD123", orders.get(0).getValueString("orderNo"));
assertEquals("ORD124", orders.get(1).getValueString("orderNo"));
List<QRecord> orderLines = TestUtils.queryTable(qInstance, TestUtils.TABLE_NAME_LINE_ITEM);
assertEquals(2, orderLines.size());
assertEquals(1, orderLines.get(0).getValueInteger("orderId"));
assertEquals(1, orderLines.get(1).getValueInteger("orderId"));
assertEquals("BASIC1", orderLines.get(0).getValueString("sku"));
assertEquals("BASIC2", orderLines.get(1).getValueString("sku"));
List<QRecord> orderExtrinsics = TestUtils.queryTable(qInstance, TestUtils.TABLE_NAME_ORDER_EXTRINSIC);
assertEquals(4, orderExtrinsics.size());
assertEquals(1, orderExtrinsics.get(0).getValueInteger("orderId"));
assertEquals(1, orderExtrinsics.get(1).getValueInteger("orderId"));
assertEquals(1, orderExtrinsics.get(2).getValueInteger("orderId"));
assertEquals(2, orderExtrinsics.get(3).getValueInteger("orderId"));
assertEquals("MY-FIELD-1", orderExtrinsics.get(0).getValueString("key"));
assertEquals("MY-FIELD-2", orderExtrinsics.get(1).getValueString("key"));
assertEquals("MY-FIELD-3", orderExtrinsics.get(2).getValueString("key"));
assertEquals("YOUR-FIELD-1", orderExtrinsics.get(3).getValueString("key"));
assertEquals("MY-VALUE-1", orderExtrinsics.get(0).getValueString("value"));
assertEquals("MY-VALUE-2", orderExtrinsics.get(1).getValueString("value"));
assertEquals("MY-VALUE-3", orderExtrinsics.get(2).getValueString("value"));
assertEquals("YOUR-VALUE-1", orderExtrinsics.get(3).getValueString("value"));
List<QRecord> lineItemExtrinsics = TestUtils.queryTable(qInstance, TestUtils.TABLE_NAME_LINE_ITEM_EXTRINSIC);
assertEquals(3, lineItemExtrinsics.size());
assertEquals(1, lineItemExtrinsics.get(0).getValueInteger("lineItemId"));
assertEquals(2, lineItemExtrinsics.get(1).getValueInteger("lineItemId"));
assertEquals(2, lineItemExtrinsics.get(2).getValueInteger("lineItemId"));
assertEquals("LINE-EXT-1.1", lineItemExtrinsics.get(0).getValueString("key"));
assertEquals("LINE-EXT-2.1", lineItemExtrinsics.get(1).getValueString("key"));
assertEquals("LINE-EXT-2.2", lineItemExtrinsics.get(2).getValueString("key"));
assertEquals("LINE-VAL-1", lineItemExtrinsics.get(0).getValueString("value"));
assertEquals("LINE-VAL-2", lineItemExtrinsics.get(1).getValueString("value"));
assertEquals("LINE-VAL-3", lineItemExtrinsics.get(2).getValueString("value"));
}
}

View File

@ -91,6 +91,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.reporting.ReportType;
import com.kingsrook.qqq.backend.core.model.metadata.security.FieldSecurityLock;
import com.kingsrook.qqq.backend.core.model.metadata.security.QSecurityKeyType;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
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.model.metadata.tables.UniqueKey;
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.AutomationStatusTracking;
@ -126,10 +127,12 @@ public class TestUtils
public static final String APP_NAME_PEOPLE = "peopleApp";
public static final String APP_NAME_MISCELLANEOUS = "miscellaneous";
public static final String TABLE_NAME_PERSON = "person";
public static final String TABLE_NAME_SHAPE = "shape";
public static final String TABLE_NAME_ORDER = "order";
public static final String TABLE_NAME_LINE_ITEM = "orderLine";
public static final String TABLE_NAME_PERSON = "person";
public static final String TABLE_NAME_SHAPE = "shape";
public static final String TABLE_NAME_ORDER = "order";
public static final String TABLE_NAME_LINE_ITEM = "orderLine";
public static final String TABLE_NAME_LINE_ITEM_EXTRINSIC = "orderLineExtrinsic";
public static final String TABLE_NAME_ORDER_EXTRINSIC = "orderExtrinsic";
public static final String PROCESS_NAME_GREET_PEOPLE = "greet";
public static final String PROCESS_NAME_GREET_PEOPLE_INTERACTIVE = "greetInteractive";
@ -185,8 +188,12 @@ public class TestUtils
qInstance.addTable(defineTableBasepull());
qInstance.addTable(defineTableOrder());
qInstance.addTable(defineTableLineItem());
qInstance.addTable(defineTableLineItemExtrinsic());
qInstance.addTable(defineTableOrderExtrinsic());
qInstance.addJoin(defineJoinOrderLineItem());
qInstance.addJoin(defineJoinLineItemLineItemExtrinsic());
qInstance.addJoin(defineJoinOrderOrderExtrinsic());
qInstance.addPossibleValueSource(defineAutomationStatusPossibleValueSource());
qInstance.addPossibleValueSource(defineStatesPossibleValueSource());
@ -538,9 +545,12 @@ public class TestUtils
.withRecordSecurityLock(new RecordSecurityLock()
.withSecurityKeyType(SECURITY_KEY_TYPE_STORE)
.withFieldName("storeId"))
.withAssociation(new Association().withName("orderLine").withAssociatedTableName(TABLE_NAME_LINE_ITEM).withJoinName("orderLineItem"))
.withAssociation(new Association().withName("extrinsics").withAssociatedTableName(TABLE_NAME_ORDER_EXTRINSIC).withJoinName("orderOrderExtrinsic"))
.withField(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false))
.withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME).withIsEditable(false))
.withField(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME).withIsEditable(false))
.withField(new QFieldMetaData("orderNo", QFieldType.STRING))
.withField(new QFieldMetaData("orderDate", QFieldType.DATE))
.withField(new QFieldMetaData("storeId", QFieldType.INTEGER))
.withField(new QFieldMetaData("total", QFieldType.DECIMAL).withDisplayFormat(DisplayFormat.CURRENCY).withFieldSecurityLock(new FieldSecurityLock()
@ -561,6 +571,7 @@ public class TestUtils
.withName(TABLE_NAME_LINE_ITEM)
.withBackendName(MEMORY_BACKEND_NAME)
.withPrimaryKeyField("id")
.withAssociation(new Association().withName("extrinsics").withAssociatedTableName(TABLE_NAME_LINE_ITEM_EXTRINSIC).withJoinName("lineItemLineItemExtrinsic"))
.withField(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false))
.withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME).withIsEditable(false))
.withField(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME).withIsEditable(false))
@ -572,6 +583,44 @@ public class TestUtils
/*******************************************************************************
** Define the lineItemExtrinsic table used in standard tests.
*******************************************************************************/
public static QTableMetaData defineTableLineItemExtrinsic()
{
return new QTableMetaData()
.withName(TABLE_NAME_LINE_ITEM_EXTRINSIC)
.withBackendName(MEMORY_BACKEND_NAME)
.withPrimaryKeyField("id")
.withField(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false))
.withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME).withIsEditable(false))
.withField(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME).withIsEditable(false))
.withField(new QFieldMetaData("lineItemId", QFieldType.INTEGER))
.withField(new QFieldMetaData("key", QFieldType.STRING))
.withField(new QFieldMetaData("value", QFieldType.STRING));
}
/*******************************************************************************
** Define the orderExtrinsic table used in standard tests.
*******************************************************************************/
public static QTableMetaData defineTableOrderExtrinsic()
{
return new QTableMetaData()
.withName(TABLE_NAME_ORDER_EXTRINSIC)
.withBackendName(MEMORY_BACKEND_NAME)
.withPrimaryKeyField("id")
.withField(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false))
.withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME).withIsEditable(false))
.withField(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME).withIsEditable(false))
.withField(new QFieldMetaData("orderId", QFieldType.INTEGER))
.withField(new QFieldMetaData("key", QFieldType.STRING))
.withField(new QFieldMetaData("value", QFieldType.STRING));
}
/*******************************************************************************
**
*******************************************************************************/
@ -588,6 +637,38 @@ public class TestUtils
/*******************************************************************************
**
*******************************************************************************/
public static QJoinMetaData defineJoinLineItemLineItemExtrinsic()
{
return new QJoinMetaData()
.withName("lineItemLineItemExtrinsic")
.withType(JoinType.ONE_TO_MANY)
.withLeftTable(TABLE_NAME_LINE_ITEM)
.withRightTable(TABLE_NAME_LINE_ITEM_EXTRINSIC)
.withJoinOn(new JoinOn("id", "lineItemId"))
.withOrderBy(new QFilterOrderBy("key"));
}
/*******************************************************************************
**
*******************************************************************************/
public static QJoinMetaData defineJoinOrderOrderExtrinsic()
{
return new QJoinMetaData()
.withName("orderOrderExtrinsic")
.withType(JoinType.ONE_TO_MANY)
.withLeftTable(TABLE_NAME_ORDER)
.withRightTable(TABLE_NAME_ORDER_EXTRINSIC)
.withJoinOn(new JoinOn("id", "orderId"))
.withOrderBy(new QFilterOrderBy("key"));
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -26,6 +26,7 @@ import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import com.kingsrook.qqq.api.model.APIVersion;
import com.kingsrook.qqq.api.model.APIVersionRange;
import com.kingsrook.qqq.api.model.actions.GenerateOpenApiSpecInput;
@ -66,8 +67,10 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValue;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Association;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Capability;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.YamlUtils;
@ -204,7 +207,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
boolean updateCapability = table.isCapabilityEnabled(tableBackend, Capability.TABLE_UPDATE);
boolean deleteCapability = table.isCapabilityEnabled(tableBackend, Capability.TABLE_DELETE);
boolean insertCapability = table.isCapabilityEnabled(tableBackend, Capability.TABLE_INSERT);
boolean countCapability = table.isCapabilityEnabled(tableBackend, Capability.TABLE_COUNT);
boolean countCapability = table.isCapabilityEnabled(tableBackend, Capability.TABLE_COUNT); // todo - look at this - if table doesn't have count, don't include it in its input/output, etc
if(!queryCapability && !getCapability && !updateCapability && !deleteCapability && !insertCapability)
{
@ -221,6 +224,9 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
String primaryKeyApiName = ApiFieldMetaData.getEffectiveApiFieldName(primaryKeyField);
List<QFieldMetaData> tableApiFields = new GetTableApiFieldsAction().execute(new GetTableApiFieldsInput().withTableName(tableName).withVersion(version)).getFields();
///////////////////////////////
// permissions for the table //
///////////////////////////////
String tableReadPermissionName = PermissionsHelper.getTablePermissionName(tableName, TablePermissionSubType.READ);
if(StringUtils.hasContent(tableReadPermissionName))
{
@ -256,13 +262,15 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
.withName(tableLabel)
.withDescription("Operations on the " + tableLabel + " table."));
//////////////////////////////////////
// build the schemas for this table //
//////////////////////////////////////
//////////////////////////////////////////////////////////////////
// build the schemas for this table //
// start with the full table minus its pkey (e.g., for posting) //
//////////////////////////////////////////////////////////////////
LinkedHashMap<String, Schema> tableFieldsWithoutPrimaryKey = new LinkedHashMap<>();
componentSchemas.put(tableApiName + "WithoutPrimaryKey", new Schema()
Schema tableWithoutPrimaryKeySchema = new Schema()
.withType("object")
.withProperties(tableFieldsWithoutPrimaryKey));
.withProperties(tableFieldsWithoutPrimaryKey);
componentSchemas.put(tableApiName + "WithoutPrimaryKey", tableWithoutPrimaryKeySchema);
for(QFieldMetaData field : tableApiFields)
{
@ -271,46 +279,26 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
continue;
}
String apiFieldName = ApiFieldMetaData.getEffectiveApiFieldName(field);
Schema fieldSchema = new Schema()
.withType(getFieldType(table.getField(field.getName())))
.withFormat(getFieldFormat(table.getField(field.getName())))
.withDescription(field.getLabel() + " for the " + tableLabel + ".");
if(StringUtils.hasContent(field.getPossibleValueSourceName()))
{
QPossibleValueSource possibleValueSource = qInstance.getPossibleValueSource(field.getPossibleValueSourceName());
if(QPossibleValueSourceType.ENUM.equals(possibleValueSource.getType()))
{
List<String> enumValues = new ArrayList<>();
for(QPossibleValue<?> enumValue : possibleValueSource.getEnumValues())
{
enumValues.add(enumValue.getId() + "=" + enumValue.getLabel());
}
fieldSchema.setEnumValues(enumValues);
}
else if(QPossibleValueSourceType.TABLE.equals(possibleValueSource.getType()))
{
QTableMetaData sourceTable = qInstance.getTable(possibleValueSource.getTableName());
fieldSchema.setDescription(fieldSchema.getDescription() + " Values in this field come from the primary key of the " + sourceTable.getLabel() + " table");
}
}
tableFieldsWithoutPrimaryKey.put(apiFieldName, fieldSchema);
Schema fieldSchema = getFieldSchema(table, field);
tableFieldsWithoutPrimaryKey.put(ApiFieldMetaData.getEffectiveApiFieldName(field), fieldSchema);
}
//////////////////////////////////
// recursively add associations //
//////////////////////////////////
addAssociations(table, tableWithoutPrimaryKeySchema);
/////////////////////////////////////////////////
// full version of table (w/o pkey + the pkey) //
/////////////////////////////////////////////////
componentSchemas.put(tableApiName, new Schema()
.withType("object")
.withAllOf(ListBuilder.of(new Schema().withRef("#/components/schemas/" + tableApiName + "WithoutPrimaryKey")))
.withProperties(MapBuilder.of(
primaryKeyApiName, new Schema()
.withType(getFieldType(table.getField(primaryKeyName)))
.withFormat(getFieldFormat(table.getField(primaryKeyName)))
.withDescription(primaryKeyLabel + " for the " + tableLabel + ". Primary Key.")
))
);
.withProperties(MapBuilder.of(primaryKeyApiName, getFieldSchema(table, table.getField(primaryKeyName)))));
//////////////////////////////////////////////////////////////////////////////
// table as a search result (the base search result, plus the table itself) //
//////////////////////////////////////////////////////////////////////////////
componentSchemas.put(tableApiName + "SearchResult", new Schema()
.withType("object")
.withAllOf(ListBuilder.of(new Schema().withRef("#/components/schemas/baseSearchResultFields")))
@ -577,6 +565,59 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
/*******************************************************************************
**
*******************************************************************************/
private static void addAssociations(QTableMetaData table, Schema tableWithoutPrimaryKeySchema)
{
for(Association association : CollectionUtils.nonNullList(table.getAssociations()))
{
String associatedTableName = association.getAssociatedTableName();
QTableMetaData associatedTable = QContext.getQInstance().getTable(associatedTableName);
ApiTableMetaData associatedApiTableMetaData = Objects.requireNonNullElse(ApiTableMetaData.of(associatedTable), new ApiTableMetaData());
String associatedTableApiName = StringUtils.hasContent(associatedApiTableMetaData.getApiTableName()) ? associatedApiTableMetaData.getApiTableName() : associatedTableName;
tableWithoutPrimaryKeySchema.getProperties().put(association.getName(), new Schema()
.withType("array")
.withItems(new Schema().withRef("#/components/schemas/" + associatedTableApiName)));
}
}
/*******************************************************************************
**
*******************************************************************************/
private Schema getFieldSchema(QTableMetaData table, QFieldMetaData field)
{
Schema fieldSchema = new Schema()
.withType(getFieldType(table.getField(field.getName())))
.withFormat(getFieldFormat(table.getField(field.getName())))
.withDescription(field.getLabel() + " for the " + table.getLabel() + ".");
if(StringUtils.hasContent(field.getPossibleValueSourceName()))
{
QPossibleValueSource possibleValueSource = QContext.getQInstance().getPossibleValueSource(field.getPossibleValueSourceName());
if(QPossibleValueSourceType.ENUM.equals(possibleValueSource.getType()))
{
List<String> enumValues = new ArrayList<>();
for(QPossibleValue<?> enumValue : possibleValueSource.getEnumValues())
{
enumValues.add(enumValue.getId() + "=" + enumValue.getLabel());
}
fieldSchema.setEnumValues(enumValues);
}
else if(QPossibleValueSourceType.TABLE.equals(possibleValueSource.getType()))
{
QTableMetaData sourceTable = QContext.getQInstance().getTable(possibleValueSource.getTableName());
fieldSchema.setDescription(fieldSchema.getDescription() + " Values in this field come from the primary key of the " + sourceTable.getLabel() + " table");
}
}
return fieldSchema;
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -32,11 +32,16 @@ import java.util.stream.Collectors;
import com.kingsrook.qqq.api.javalin.QBadRequestException;
import com.kingsrook.qqq.api.model.actions.GetTableApiFieldsInput;
import com.kingsrook.qqq.api.model.metadata.fields.ApiFieldMetaData;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
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.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.Pair;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import org.json.JSONArray;
import org.json.JSONObject;
@ -94,6 +99,13 @@ public class QRecordApiAdapter
List<String> unrecognizedFieldNames = new ArrayList<>();
QRecord qRecord = new QRecord();
Map<String, Association> associationMap = new HashMap<>();
QTableMetaData table = QContext.getQInstance().getTable(tableName);
for(Association association : CollectionUtils.nonNullList(table.getAssociations()))
{
associationMap.put(association.getName(), association);
}
//////////////////////////////////////////
// iterate over keys in the json object //
//////////////////////////////////////////
@ -117,6 +129,30 @@ public class QRecordApiAdapter
qRecord.setValue(field.getName(), value);
}
}
else if(associationMap.containsKey(jsonKey))
{
Association association = associationMap.get(jsonKey);
Object value = jsonObject.get(jsonKey);
if(value instanceof JSONArray jsonArray)
{
for(Object subObject : jsonArray)
{
if(subObject instanceof JSONObject subJsonObject)
{
QRecord subRecord = apiJsonObjectToQRecord(subJsonObject, association.getAssociatedTableName(), apiVersion);
qRecord.withAssociatedRecord(association.getName(), subRecord);
}
else
{
throw (new QBadRequestException("Found a " + value.getClass().getSimpleName() + " in the array under key " + jsonKey + ", but a JSON object is required here."));
}
}
}
else
{
throw (new QBadRequestException("Found a " + value.getClass().getSimpleName() + " at key " + jsonKey + ", but a JSON array is required here."));
}
}
else
{
///////////////////////////////////////////////////

View File

@ -76,6 +76,7 @@ import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.ExceptionUtils;
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
@ -253,7 +254,7 @@ public class QJavalinApiHandler
String version = context.pathParam("version");
GenerateOpenApiSpecInput input = new GenerateOpenApiSpecInput().withVersion(version);
if(context.pathParam("tableName") != null)
if(StringUtils.hasContent(context.pathParam("tableName")))
{
input.setTableName(context.pathParam("tableName"));
}
@ -273,9 +274,10 @@ public class QJavalinApiHandler
/*******************************************************************************
**
*******************************************************************************/
public static void setupSession(Context context, AbstractActionInput input) throws QModuleDispatchException, QAuthenticationException
public static void setupSession(Context context, AbstractActionInput input, String version) throws QModuleDispatchException, QAuthenticationException
{
QJavalinImplementation.setupSession(context, input);
QSession session = QJavalinImplementation.setupSession(context, input);
session.setValue("apiVersion", version);
}
@ -296,8 +298,8 @@ public class QJavalinApiHandler
GetInput getInput = new GetInput();
setupSession(context, getInput);
QJavalinAccessLogger.logStart("get", logPair("table", tableName), logPair("primaryKey", primaryKey));
setupSession(context, getInput, version);
QJavalinAccessLogger.logStart("apiGet", logPair("table", tableName), logPair("primaryKey", primaryKey));
getInput.setTableName(tableName);
// i think not for api... getInput.setShouldGenerateDisplayValues(true);
@ -354,7 +356,7 @@ public class QJavalinApiHandler
String tableName = table.getName();
QueryInput queryInput = new QueryInput();
setupSession(context, queryInput);
setupSession(context, queryInput, version);
QJavalinAccessLogger.logStart("apiQuery", logPair("table", tableName));
queryInput.setTableName(tableName);
@ -785,8 +787,8 @@ public class QJavalinApiHandler
InsertInput insertInput = new InsertInput();
setupSession(context, insertInput);
QJavalinAccessLogger.logStart("insert", logPair("table", tableName));
setupSession(context, insertInput, version);
QJavalinAccessLogger.logStart("apiInsert", logPair("table", tableName));
insertInput.setTableName(tableName);
@ -852,8 +854,8 @@ public class QJavalinApiHandler
InsertInput insertInput = new InsertInput();
setupSession(context, insertInput);
QJavalinAccessLogger.logStart("bulkInsert", logPair("table", tableName));
setupSession(context, insertInput, version);
QJavalinAccessLogger.logStart("apiBulkInsert", logPair("table", tableName));
insertInput.setTableName(tableName);
@ -958,8 +960,8 @@ public class QJavalinApiHandler
UpdateInput updateInput = new UpdateInput();
setupSession(context, updateInput);
QJavalinAccessLogger.logStart("bulkUpdate", logPair("table", tableName));
setupSession(context, updateInput, version);
QJavalinAccessLogger.logStart("apiBulkUpdate", logPair("table", tableName));
updateInput.setTableName(tableName);
@ -1063,8 +1065,8 @@ public class QJavalinApiHandler
DeleteInput deleteInput = new DeleteInput();
setupSession(context, deleteInput);
QJavalinAccessLogger.logStart("bulkDelete", logPair("table", tableName));
setupSession(context, deleteInput, version);
QJavalinAccessLogger.logStart("apiBulkDelete", logPair("table", tableName));
deleteInput.setTableName(tableName);
@ -1182,8 +1184,8 @@ public class QJavalinApiHandler
UpdateInput updateInput = new UpdateInput();
setupSession(context, updateInput);
QJavalinAccessLogger.logStart("update", logPair("table", tableName));
setupSession(context, updateInput, version);
QJavalinAccessLogger.logStart("apiUpdate", logPair("table", tableName));
updateInput.setTableName(tableName);
@ -1268,8 +1270,8 @@ public class QJavalinApiHandler
DeleteInput deleteInput = new DeleteInput();
setupSession(context, deleteInput);
QJavalinAccessLogger.logStart("delete", logPair("table", tableName));
setupSession(context, deleteInput, version);
QJavalinAccessLogger.logStart("apiDelete", logPair("table", tableName));
deleteInput.setTableName(tableName);
deleteInput.setPrimaryKeys(List.of(primaryKey));

View File

@ -27,6 +27,7 @@ import com.kingsrook.qqq.api.model.APIVersion;
import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaData;
import com.kingsrook.qqq.api.model.metadata.fields.ApiFieldMetaData;
import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaData;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy;
import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
@ -34,6 +35,10 @@ import com.kingsrook.qqq.backend.core.model.metadata.authentication.Auth0Authent
import com.kingsrook.qqq.backend.core.model.metadata.fields.DisplayFormat;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn;
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinType;
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.model.metadata.tables.UniqueKey;
import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryBackendModule;
@ -45,10 +50,15 @@ import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.Mem
public class TestUtils
{
public static final String MEMORY_BACKEND_NAME = "memory";
public static final String TABLE_NAME_PERSON = "person";
public static final String V2023_Q1 = "2023.Q1";
public static final String TABLE_NAME_PERSON = "person";
public static final String TABLE_NAME_ORDER = "order";
public static final String TABLE_NAME_LINE_ITEM = "orderLine";
public static final String TABLE_NAME_LINE_ITEM_EXTRINSIC = "orderLineExtrinsic";
public static final String TABLE_NAME_ORDER_EXTRINSIC = "orderExtrinsic";
public static final String V2022_Q4 = "2022.Q4";
public static final String V2023_Q1 = "2023.Q1";
public static final String V2023_Q2 = "2023.Q2";
public static final String CURRENT_API_VERSION = V2023_Q1;
@ -64,6 +74,15 @@ public class TestUtils
qInstance.addBackend(defineMemoryBackend());
qInstance.addTable(defineTablePerson());
qInstance.addTable(defineTableOrder());
qInstance.addTable(defineTableLineItem());
qInstance.addTable(defineTableLineItemExtrinsic());
qInstance.addTable(defineTableOrderExtrinsic());
qInstance.addJoin(defineJoinOrderLineItem());
qInstance.addJoin(defineJoinLineItemLineItemExtrinsic());
qInstance.addJoin(defineJoinOrderOrderExtrinsic());
qInstance.setAuthentication(new Auth0AuthenticationMetaData().withType(QAuthenticationType.FULLY_ANONYMOUS).withName("anonymous"));
qInstance.withMiddlewareMetaData(new ApiInstanceMetaData()
@ -139,4 +158,137 @@ public class TestUtils
;
}
/*******************************************************************************
** Define the order table used in standard tests.
*******************************************************************************/
public static QTableMetaData defineTableOrder()
{
return new QTableMetaData()
.withName(TABLE_NAME_ORDER)
.withBackendName(MEMORY_BACKEND_NAME)
.withMiddlewareMetaData(new ApiTableMetaData().withInitialVersion(V2022_Q4))
.withPrimaryKeyField("id")
.withAssociation(new Association().withName("orderLines").withAssociatedTableName(TABLE_NAME_LINE_ITEM).withJoinName("orderLineItem"))
.withAssociation(new Association().withName("extrinsics").withAssociatedTableName(TABLE_NAME_ORDER_EXTRINSIC).withJoinName("orderOrderExtrinsic"))
.withField(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false))
.withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME).withIsEditable(false))
.withField(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME).withIsEditable(false))
.withField(new QFieldMetaData("orderNo", QFieldType.STRING))
.withField(new QFieldMetaData("orderDate", QFieldType.DATE))
.withField(new QFieldMetaData("storeId", QFieldType.INTEGER))
.withField(new QFieldMetaData("total", QFieldType.DECIMAL).withDisplayFormat(DisplayFormat.CURRENCY));
}
/*******************************************************************************
** Define the lineItem table used in standard tests.
*******************************************************************************/
public static QTableMetaData defineTableLineItem()
{
return new QTableMetaData()
.withName(TABLE_NAME_LINE_ITEM)
.withBackendName(MEMORY_BACKEND_NAME)
.withMiddlewareMetaData(new ApiTableMetaData().withInitialVersion(V2022_Q4))
.withPrimaryKeyField("id")
.withAssociation(new Association().withName("extrinsics").withAssociatedTableName(TABLE_NAME_LINE_ITEM_EXTRINSIC).withJoinName("lineItemLineItemExtrinsic"))
.withField(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false))
.withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME).withIsEditable(false))
.withField(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME).withIsEditable(false))
.withField(new QFieldMetaData("orderId", QFieldType.INTEGER))
.withField(new QFieldMetaData("lineNumber", QFieldType.STRING))
.withField(new QFieldMetaData("sku", QFieldType.STRING))
.withField(new QFieldMetaData("quantity", QFieldType.INTEGER));
}
/*******************************************************************************
** Define the lineItemExtrinsic table used in standard tests.
*******************************************************************************/
public static QTableMetaData defineTableLineItemExtrinsic()
{
return new QTableMetaData()
.withName(TABLE_NAME_LINE_ITEM_EXTRINSIC)
.withBackendName(MEMORY_BACKEND_NAME)
.withMiddlewareMetaData(new ApiTableMetaData().withInitialVersion(V2022_Q4))
.withPrimaryKeyField("id")
.withField(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false))
.withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME).withIsEditable(false))
.withField(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME).withIsEditable(false))
.withField(new QFieldMetaData("lineItemId", QFieldType.INTEGER))
.withField(new QFieldMetaData("key", QFieldType.STRING))
.withField(new QFieldMetaData("value", QFieldType.STRING));
}
/*******************************************************************************
** Define the orderExtrinsic table used in standard tests.
*******************************************************************************/
public static QTableMetaData defineTableOrderExtrinsic()
{
return new QTableMetaData()
.withName(TABLE_NAME_ORDER_EXTRINSIC)
.withBackendName(MEMORY_BACKEND_NAME)
.withMiddlewareMetaData(new ApiTableMetaData().withInitialVersion(V2022_Q4))
.withPrimaryKeyField("id")
.withField(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false))
.withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME).withIsEditable(false))
.withField(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME).withIsEditable(false))
.withField(new QFieldMetaData("orderId", QFieldType.INTEGER))
.withField(new QFieldMetaData("key", QFieldType.STRING))
.withField(new QFieldMetaData("value", QFieldType.STRING));
}
/*******************************************************************************
**
*******************************************************************************/
public static QJoinMetaData defineJoinOrderLineItem()
{
return new QJoinMetaData()
.withName("orderLineItem")
.withType(JoinType.ONE_TO_MANY)
.withLeftTable(TABLE_NAME_ORDER)
.withRightTable(TABLE_NAME_LINE_ITEM)
.withJoinOn(new JoinOn("id", "orderId"))
.withOrderBy(new QFilterOrderBy("lineNumber"));
}
/*******************************************************************************
**
*******************************************************************************/
public static QJoinMetaData defineJoinLineItemLineItemExtrinsic()
{
return new QJoinMetaData()
.withName("lineItemLineItemExtrinsic")
.withType(JoinType.ONE_TO_MANY)
.withLeftTable(TABLE_NAME_LINE_ITEM)
.withRightTable(TABLE_NAME_LINE_ITEM_EXTRINSIC)
.withJoinOn(new JoinOn("id", "lineItemId"))
.withOrderBy(new QFilterOrderBy("key"));
}
/*******************************************************************************
**
*******************************************************************************/
public static QJoinMetaData defineJoinOrderOrderExtrinsic()
{
return new QJoinMetaData()
.withName("orderOrderExtrinsic")
.withType(JoinType.ONE_TO_MANY)
.withLeftTable(TABLE_NAME_ORDER)
.withRightTable(TABLE_NAME_ORDER_EXTRINSIC)
.withJoinOn(new JoinOn("id", "orderId"))
.withOrderBy(new QFilterOrderBy("key"));
}
}

View File

@ -37,6 +37,7 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
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.QFilterOrderBy;
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;
@ -59,6 +60,7 @@ 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.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
/*******************************************************************************
@ -407,12 +409,55 @@ class QJavalinApiHandlerTest extends BaseTest
JSONObject jsonObject = new JSONObject(response.getBody());
assertEquals(1, jsonObject.getInt("id"));
QRecord record = getPersonRecord(1);
QRecord record = getRecord(TestUtils.TABLE_NAME_PERSON, 1);
assertEquals("Moe", record.getValueString("firstName"));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testInsert201WithAssociatedRecords() throws QException
{
HttpResponse<String> response = Unirest.post(BASE_URL + "/api/" + VERSION + "/order/")
.body("""
{"orderNo": "ORD123", "storeId": 47, "orderLines":
[
{"lineNumber": 1, "sku": "BASIC1", "quantity": 17, "extrinsics": [{"key": "size", "value": "Large"}]},
{"lineNumber": 2, "sku": "BASIC2", "quantity": 23}
], "extrinsics":
[
{"key": "storeName", "value": "My Shopify"},
{"key": "shopifyOrderNo", "value": "#2820503"}
]
}
""")
.asString();
System.out.println(response.getBody());
assertEquals(HttpStatus.CREATED_201, response.getStatus());
JSONObject jsonObject = new JSONObject(response.getBody());
assertEquals(1, jsonObject.getInt("id"));
QRecord record = getRecord(TestUtils.TABLE_NAME_ORDER, 1);
assertEquals("ORD123", record.getValueString("orderNo"));
List<QRecord> lines = queryTable(TestUtils.TABLE_NAME_LINE_ITEM);
assertEquals(2, lines.size());
assertTrue(lines.stream().allMatch(r -> r.getValueInteger("orderId").equals(1)));
assertTrue(lines.stream().anyMatch(r -> r.getValueString("sku").equals("BASIC1")));
assertTrue(lines.stream().anyMatch(r -> r.getValueString("sku").equals("BASIC2")));
List<QRecord> orderExtrinsics = queryTable(TestUtils.TABLE_NAME_ORDER_EXTRINSIC);
assertEquals(2, orderExtrinsics.size());
assertTrue(orderExtrinsics.stream().allMatch(r -> r.getValueInteger("orderId").equals(1)));
assertTrue(orderExtrinsics.stream().anyMatch(r -> r.getValueString("key").equals("storeName") && r.getValueString("value").equals("My Shopify")));
assertTrue(orderExtrinsics.stream().anyMatch(r -> r.getValueString("key").equals("shopifyOrderNo") && r.getValueString("value").equals("#2820503")));
}
/*******************************************************************************
**
*******************************************************************************/
@ -462,7 +507,7 @@ class QJavalinApiHandlerTest extends BaseTest
///////////////////////////////////
// assert it didn't get inserted //
///////////////////////////////////
QRecord personRecord = getPersonRecord(1);
QRecord personRecord = getRecord(TestUtils.TABLE_NAME_PERSON, 1);
assertNull(personRecord);
///////////////////////////////////////////
@ -511,16 +556,16 @@ class QJavalinApiHandlerTest extends BaseTest
assertEquals(HttpStatus.BAD_REQUEST_400, jsonArray.getJSONObject(3).getInt("statusCode"));
assertEquals("Error inserting Person: Another record already exists with this Email", jsonArray.getJSONObject(3).getString("error"));
QRecord record = getPersonRecord(1);
QRecord record = getRecord(TestUtils.TABLE_NAME_PERSON, 1);
assertEquals("Moe", record.getValueString("firstName"));
record = getPersonRecord(2);
record = getRecord(TestUtils.TABLE_NAME_PERSON, 2);
assertEquals("Barney", record.getValueString("firstName"));
record = getPersonRecord(3);
record = getRecord(TestUtils.TABLE_NAME_PERSON, 3);
assertEquals("CM", record.getValueString("firstName"));
record = getPersonRecord(4);
record = getRecord(TestUtils.TABLE_NAME_PERSON, 4);
assertNull(record);
}
@ -559,7 +604,7 @@ class QJavalinApiHandlerTest extends BaseTest
/////////////////////////////////
// assert nothing got inserted //
/////////////////////////////////
QRecord personRecord = getPersonRecord(1);
QRecord personRecord = getRecord(TestUtils.TABLE_NAME_PERSON, 1);
assertNull(personRecord);
//////////////////////////////////////////
@ -592,7 +637,7 @@ class QJavalinApiHandlerTest extends BaseTest
assertEquals(HttpStatus.NO_CONTENT_204, response.getStatus());
assertFalse(StringUtils.hasContent(response.getBody()));
QRecord record = getPersonRecord(1);
QRecord record = getRecord(TestUtils.TABLE_NAME_PERSON, 1);
assertEquals("Charles", record.getValueString("firstName"));
}
@ -665,7 +710,7 @@ class QJavalinApiHandlerTest extends BaseTest
///////////////////////////////////
// assert it didn't get updated. //
///////////////////////////////////
QRecord personRecord = getPersonRecord(1);
QRecord personRecord = getRecord(TestUtils.TABLE_NAME_PERSON, 1);
assertEquals("Mo", personRecord.getValueString("firstName"));
response = Unirest.patch(BASE_URL + "/api/" + VERSION + "/person/1")
@ -706,10 +751,10 @@ class QJavalinApiHandlerTest extends BaseTest
assertEquals(HttpStatus.BAD_REQUEST_400, jsonArray.getJSONObject(2).getInt("statusCode"));
assertEquals("Error updating Person: Missing value in primary key field", jsonArray.getJSONObject(2).getString("error"));
QRecord record = getPersonRecord(1);
QRecord record = getRecord(TestUtils.TABLE_NAME_PERSON, 1);
assertEquals("homer@simpson.com", record.getValueString("email"));
record = getPersonRecord(2);
record = getRecord(TestUtils.TABLE_NAME_PERSON, 2);
assertEquals("marge@simpson.com", record.getValueString("email"));
QueryInput queryInput = new QueryInput();
@ -754,7 +799,7 @@ class QJavalinApiHandlerTest extends BaseTest
////////////////////////////////
// assert nothing got updated //
////////////////////////////////
QRecord personRecord = getPersonRecord(1);
QRecord personRecord = getRecord(TestUtils.TABLE_NAME_PERSON, 1);
assertNull(personRecord);
//////////////////////////////////////////
@ -834,7 +879,7 @@ class QJavalinApiHandlerTest extends BaseTest
////////////////////////////////
// assert nothing got deleted //
////////////////////////////////
QRecord personRecord = getPersonRecord(1);
QRecord personRecord = getRecord(TestUtils.TABLE_NAME_PERSON, 1);
assertNull(personRecord);
//////////////////////////////////////////
@ -877,7 +922,7 @@ class QJavalinApiHandlerTest extends BaseTest
assertEquals(HttpStatus.NO_CONTENT_204, response.getStatus());
assertFalse(StringUtils.hasContent(response.getBody()));
QRecord record = getPersonRecord(1);
QRecord record = getRecord(TestUtils.TABLE_NAME_PERSON, 1);
assertNull(record);
}
@ -895,6 +940,43 @@ 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);
assertEquals(1, queryTable(TestUtils.TABLE_NAME_ORDER).size());
assertEquals(4, queryTable(TestUtils.TABLE_NAME_LINE_ITEM_EXTRINSIC).size());
assertEquals(3, queryTable(TestUtils.TABLE_NAME_LINE_ITEM).size());
assertEquals(1, queryTable(TestUtils.TABLE_NAME_ORDER_EXTRINSIC).size());
HttpResponse<String> response = Unirest.delete(BASE_URL + "/api/" + VERSION + "/order/1").asString();
assertEquals(HttpStatus.NO_CONTENT_204, response.getStatus());
assertFalse(StringUtils.hasContent(response.getBody()));
assertEquals(0, queryTable(TestUtils.TABLE_NAME_ORDER).size());
assertEquals(0, queryTable(TestUtils.TABLE_NAME_LINE_ITEM_EXTRINSIC).size());
assertEquals(0, queryTable(TestUtils.TABLE_NAME_LINE_ITEM).size());
assertEquals(0, queryTable(TestUtils.TABLE_NAME_ORDER_EXTRINSIC).size());
}
/*******************************************************************************
**
*******************************************************************************/
@ -919,10 +1001,10 @@ class QJavalinApiHandlerTest extends BaseTest
/*******************************************************************************
**
*******************************************************************************/
private static QRecord getPersonRecord(Integer id) throws QException
private static QRecord getRecord(String tableName, Integer id) throws QException
{
GetInput getInput = new GetInput();
getInput.setTableName(TestUtils.TABLE_NAME_PERSON);
getInput.setTableName(tableName);
getInput.setPrimaryKey(id);
GetOutput getOutput = new GetAction().execute(getInput);
QRecord record = getOutput.getRecord();
@ -931,6 +1013,20 @@ class QJavalinApiHandlerTest extends BaseTest
/*******************************************************************************
**
*******************************************************************************/
private static List<QRecord> queryTable(String tableName) throws QException
{
QueryInput queryInput = new QueryInput();
queryInput.setTableName(tableName);
queryInput.setFilter(new QQueryFilter().withOrderBy(new QFilterOrderBy("id")));
QueryOutput queryOutput = new QueryAction().execute(queryInput);
return (queryOutput.getRecords());
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -411,7 +411,7 @@ public class QJavalinImplementation
/*******************************************************************************
**
*******************************************************************************/
public static void setupSession(Context context, AbstractActionInput input) throws QModuleDispatchException, QAuthenticationException
public static QSession setupSession(Context context, AbstractActionInput input) throws QModuleDispatchException, QAuthenticationException
{
QAuthenticationModuleDispatcher qAuthenticationModuleDispatcher = new QAuthenticationModuleDispatcher();
QAuthenticationModuleInterface authenticationModule = qAuthenticationModuleDispatcher.getQModule(qInstance.getAuthentication());
@ -466,6 +466,8 @@ public class QJavalinImplementation
setUserTimezoneOffsetMinutesInSession(context, session);
setUserTimezoneInSession(context, session);
return (session);
}
catch(QAuthenticationException qae)
{