Add check for records pre-delete action (for security and better errors); 404s and ids in 207s for bulk update & delete; ignore non-editable fields;

This commit is contained in:
2023-03-31 12:11:12 -05:00
parent 21e3cdd0a5
commit 084630918f
15 changed files with 690 additions and 388 deletions

View File

@ -24,7 +24,11 @@ package com.kingsrook.qqq.backend.core.actions.tables;
import java.io.Serializable; import java.io.Serializable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map;
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.audits.DMLAuditAction; import com.kingsrook.qqq.backend.core.actions.audits.DMLAuditAction;
import com.kingsrook.qqq.backend.core.actions.interfaces.DeleteInterface; import com.kingsrook.qqq.backend.core.actions.interfaces.DeleteInterface;
@ -41,12 +45,14 @@ 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.audits.AuditLevel; import com.kingsrook.qqq.backend.core.model.metadata.audits.AuditLevel;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData; 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.Association;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; 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.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
/******************************************************************************* /*******************************************************************************
@ -57,6 +63,8 @@ public class DeleteAction
{ {
private static final QLogger LOG = QLogger.getLogger(DeleteAction.class); private static final QLogger LOG = QLogger.getLogger(DeleteAction.class);
public static final String NOT_FOUND_ERROR_PREFIX = "No record was found to delete";
/******************************************************************************* /*******************************************************************************
@ -68,7 +76,6 @@ public class DeleteAction
QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher(); QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher();
QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(deleteInput.getBackend()); QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(deleteInput.getBackend());
// todo pre-customization - just get to modify the request?
if(CollectionUtils.nullSafeHasContents(deleteInput.getPrimaryKeys()) && deleteInput.getQueryFilter() != null) if(CollectionUtils.nullSafeHasContents(deleteInput.getPrimaryKeys()) && deleteInput.getQueryFilter() != null)
{ {
@ -93,12 +100,23 @@ public class DeleteAction
} }
List<QRecord> recordListForAudit = getRecordListForAuditIfNeeded(deleteInput); List<QRecord> recordListForAudit = getRecordListForAuditIfNeeded(deleteInput);
List<QRecord> recordsWithValidationErrors = validateRecordsExistAndCanBeAccessed(deleteInput, recordListForAudit);
DeleteOutput deleteOutput = deleteInterface.execute(deleteInput); DeleteOutput deleteOutput = deleteInterface.execute(deleteInput);
manageAssociations(deleteInput); /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// merge the backend's output with any validation errors we found (whose ids wouldn't have gotten into the backend delete) //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
List<QRecord> outputRecordsWithErrors = deleteOutput.getRecordsWithErrors();
if(outputRecordsWithErrors == null)
{
deleteOutput.setRecordsWithErrors(new ArrayList<>());
outputRecordsWithErrors = deleteOutput.getRecordsWithErrors();
}
// todo post-customization - can do whatever w/ the result if you want outputRecordsWithErrors.addAll(recordsWithValidationErrors);
manageAssociations(deleteInput);
new DMLAuditAction().execute(new DMLAuditInput().withTableActionInput(deleteInput).withRecordList(recordListForAudit)); new DMLAuditAction().execute(new DMLAuditInput().withTableActionInput(deleteInput).withRecordList(recordListForAudit));
@ -188,6 +206,84 @@ public class DeleteAction
/*******************************************************************************
** Note - the "can be accessed" part of this method name - it implies that
** records that you can't see because of security - that they won't be found
** by the query here, so it's the same to you as if they don't exist at all!
**
** This method, if it finds any missing records, will:
** - remove those ids from the deleteInput
** - create a QRecord with that id and a not-found error message.
*******************************************************************************/
private List<QRecord> validateRecordsExistAndCanBeAccessed(DeleteInput deleteInput, List<QRecord> oldRecordList) throws QException
{
List<QRecord> recordsWithErrors = new ArrayList<>();
QTableMetaData table = deleteInput.getTable();
QFieldMetaData primaryKeyField = table.getField(table.getPrimaryKeyField());
Set<Serializable> primaryKeysToRemoveFromInput = new HashSet<>();
List<List<Serializable>> pages = CollectionUtils.getPages(deleteInput.getPrimaryKeys(), 1000);
for(List<Serializable> page : pages)
{
List<Serializable> primaryKeysToLookup = new ArrayList<>();
for(Serializable primaryKeyValue : page)
{
if(primaryKeyValue != null)
{
primaryKeysToLookup.add(primaryKeyValue);
}
}
Map<Serializable, QRecord> lookedUpRecords = new HashMap<>();
if(CollectionUtils.nullSafeHasContents(oldRecordList))
{
for(QRecord record : oldRecordList)
{
lookedUpRecords.put(record.getValue(table.getPrimaryKeyField()), record);
}
}
else if(!primaryKeysToLookup.isEmpty())
{
QueryInput queryInput = new QueryInput();
queryInput.setTableName(table.getName());
queryInput.setFilter(new QQueryFilter(new QFilterCriteria(table.getPrimaryKeyField(), QCriteriaOperator.IN, primaryKeysToLookup)));
QueryOutput queryOutput = new QueryAction().execute(queryInput);
for(QRecord record : queryOutput.getRecords())
{
lookedUpRecords.put(record.getValue(table.getPrimaryKeyField()), record);
}
}
for(Serializable primaryKeyValue : page)
{
primaryKeyValue = ValueUtils.getValueAsFieldType(primaryKeyField.getType(), primaryKeyValue);
if(!lookedUpRecords.containsKey(primaryKeyValue))
{
QRecord recordWithError = new QRecord();
recordsWithErrors.add(recordWithError);
recordWithError.setValue(primaryKeyField.getName(), primaryKeyValue);
recordWithError.addError(NOT_FOUND_ERROR_PREFIX + " for " + primaryKeyField.getLabel() + " = " + primaryKeyValue);
primaryKeysToRemoveFromInput.add(primaryKeyValue);
}
}
/////////////////////////////////////////////////////////////////
// do one mass removal of any bad keys from the input key list //
/////////////////////////////////////////////////////////////////
if(!primaryKeysToRemoveFromInput.isEmpty())
{
deleteInput.getPrimaryKeys().removeAll(primaryKeysToRemoveFromInput);
primaryKeysToRemoveFromInput.clear();
}
}
return (recordsWithErrors);
}
/******************************************************************************* /*******************************************************************************
** For an implementation that doesn't support a queryFilter as its input, ** For an implementation that doesn't support a queryFilter as its input,
** but a scenario where a query filter was passed in - run the query, to ** but a scenario where a query filter was passed in - run the query, to

View File

@ -72,6 +72,8 @@ public class UpdateAction
{ {
private static final QLogger LOG = QLogger.getLogger(UpdateAction.class); private static final QLogger LOG = QLogger.getLogger(UpdateAction.class);
public static final String NOT_FOUND_ERROR_PREFIX = "No record was found to update";
/******************************************************************************* /*******************************************************************************
@ -193,7 +195,7 @@ public class UpdateAction
if(!lookedUpRecords.containsKey(value)) if(!lookedUpRecords.containsKey(value))
{ {
record.addError("No record was found to update for " + primaryKeyField.getLabel() + " = " + value); record.addError(NOT_FOUND_ERROR_PREFIX + " for " + primaryKeyField.getLabel() + " = " + value);
} }
} }
} }

View File

@ -27,6 +27,7 @@ import java.util.List;
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;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.utils.collections.MutableList;
/******************************************************************************* /*******************************************************************************
@ -101,7 +102,10 @@ public class DeleteInput extends AbstractTableActionInput
*******************************************************************************/ *******************************************************************************/
public void setPrimaryKeys(List<Serializable> primaryKeys) public void setPrimaryKeys(List<Serializable> primaryKeys)
{ {
this.primaryKeys = primaryKeys; ///////////////////////////////////////////////////////////////////////////////////////////////
// the action may edit this list (e.g., to remove keys w/ errors), so wrap it in MutableList //
///////////////////////////////////////////////////////////////////////////////////////////////
this.primaryKeys = new MutableList<>(primaryKeys);
} }
@ -112,7 +116,7 @@ public class DeleteInput extends AbstractTableActionInput
*******************************************************************************/ *******************************************************************************/
public DeleteInput withPrimaryKeys(List<Serializable> primaryKeys) public DeleteInput withPrimaryKeys(List<Serializable> primaryKeys)
{ {
this.primaryKeys = primaryKeys; setPrimaryKeys(primaryKeys);
return (this); return (this);
} }

View File

@ -348,7 +348,7 @@ public class BackendQueryFilterUtils
} }
} }
if(!criterion.getValues().contains(value)) if(value == null || !criterion.getValues().contains(value))
{ {
return (false); return (false);
} }

View File

@ -27,10 +27,15 @@ import java.util.Map;
import java.util.function.Supplier; import java.util.function.Supplier;
@SuppressWarnings({ "checkstyle:javadoc", "DanglingJavadoc" })
/******************************************************************************* /*******************************************************************************
** Map.of is "great", but annoying because it makes unmodifiable maps, and it ** Map.of is "great", but annoying because it makes unmodifiable maps, and it
** NPE's on nulls... So, replace it with this, which returns HashMaps, which ** NPE's on nulls... So, replace it with this, which returns HashMaps (or maps
** "don't suck" ** of the type you choose).
**
** Can use it 2 ways:
** MapBuilder.of(key, value, key2, value2, ...) => Map (a HashMap)
** MapBuilder.<KeyType ValueType>of(SomeMap::new).with(key, value).with(key2, value2)...build() => SomeMap (the type you specify)
*******************************************************************************/ *******************************************************************************/
public class MapBuilder<K, V> public class MapBuilder<K, V>
{ {

View File

@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.actions.tables;
import java.util.List; import java.util.List;
import java.util.Objects;
import com.kingsrook.qqq.backend.core.BaseTest; import com.kingsrook.qqq.backend.core.BaseTest;
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;
@ -40,8 +41,9 @@ 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.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.audits.AuditLevel; import com.kingsrook.qqq.backend.core.model.metadata.audits.AuditLevel;
import com.kingsrook.qqq.backend.core.model.metadata.audits.QAuditRules; import com.kingsrook.qqq.backend.core.model.metadata.audits.QAuditRules;
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 com.kingsrook.qqq.backend.core.utils.collections.ListBuilder;
import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNotNull;
@ -57,8 +59,6 @@ class DeleteActionTest extends BaseTest
{ {
/******************************************************************************* /*******************************************************************************
** At the core level, there isn't much that can be asserted, as it uses the
** mock implementation - just confirming that all of the "wiring" works.
** **
*******************************************************************************/ *******************************************************************************/
@Test @Test
@ -66,11 +66,17 @@ class DeleteActionTest extends BaseTest
{ {
DeleteInput request = new DeleteInput(); DeleteInput request = new DeleteInput();
request.setTableName("person"); request.setTableName("person");
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// the mock backend - it'll find a record for id=1, but not for id=2 - so we can test both a found & deleted, and a not-found here //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
request.setPrimaryKeys(List.of(1, 2)); request.setPrimaryKeys(List.of(1, 2));
DeleteOutput result = new DeleteAction().execute(request); DeleteOutput result = new DeleteAction().execute(request);
assertNotNull(result); assertNotNull(result);
assertEquals(2, result.getDeletedRecordCount()); assertEquals(1, result.getDeletedRecordCount());
assertTrue(CollectionUtils.nullSafeIsEmpty(result.getRecordsWithErrors())); assertEquals(1, result.getRecordsWithErrors().size());
assertEquals(2, result.getRecordsWithErrors().get(0).getValueInteger("id"));
assertEquals("No record was found to delete for Id = 2", result.getRecordsWithErrors().get(0).getErrors().get(0));
} }
@ -295,4 +301,102 @@ class DeleteActionTest extends BaseTest
return (queryOutput.getRecords()); return (queryOutput.getRecords());
} }
/*******************************************************************************
**
*******************************************************************************/
@Test
void testSecurityKeys() throws QException
{
QContext.getQSession().setSecurityKeyValues(MapBuilder.of(TestUtils.SECURITY_KEY_TYPE_STORE, ListBuilder.of(1)));
insert2OrdersWith3Lines3LineExtrinsicsAnd4OrderExtrinsicAssociations();
///////////////////////////////////////////////////////
// make sure we inserted the records we think we did //
///////////////////////////////////////////////////////
assertIdsExist(TestUtils.TABLE_NAME_ORDER, List.of(1, 2));
assertIdsExist(TestUtils.TABLE_NAME_LINE_ITEM, List.of(1, 2, 3));
assertIdsExist(TestUtils.TABLE_NAME_LINE_ITEM_EXTRINSIC, List.of(1, 2, 3));
assertIdsExist(TestUtils.TABLE_NAME_ORDER_EXTRINSIC, List.of(1, 2, 3, 4));
QContext.getQSession().setSecurityKeyValues(MapBuilder.of(TestUtils.SECURITY_KEY_TYPE_STORE, ListBuilder.of(2)));
//////////////////////////////////////////////////
// assert can't delete the records at any level //
//////////////////////////////////////////////////
DeleteInput deleteInput = new DeleteInput();
deleteInput.setTableName(TestUtils.TABLE_NAME_ORDER);
deleteInput.setPrimaryKeys(List.of(1));
DeleteOutput deleteOutput = new DeleteAction().execute(deleteInput);
assertEquals(0, deleteOutput.getDeletedRecordCount());
assertEquals(1, deleteOutput.getRecordsWithErrors().size());
assertEquals("No record was found to delete for Id = 1", deleteOutput.getRecordsWithErrors().get(0).getErrors().get(0));
deleteInput.setTableName(TestUtils.TABLE_NAME_LINE_ITEM);
deleteInput.setPrimaryKeys(List.of(1));
deleteOutput = new DeleteAction().execute(deleteInput);
assertEquals(0, deleteOutput.getDeletedRecordCount());
assertEquals(1, deleteOutput.getRecordsWithErrors().size());
assertEquals("No record was found to delete for Id = 1", deleteOutput.getRecordsWithErrors().get(0).getErrors().get(0));
deleteInput.setTableName(TestUtils.TABLE_NAME_LINE_ITEM_EXTRINSIC);
deleteInput.setPrimaryKeys(List.of(1));
deleteOutput = new DeleteAction().execute(deleteInput);
assertEquals(0, deleteOutput.getDeletedRecordCount());
assertEquals(1, deleteOutput.getRecordsWithErrors().size());
assertEquals("No record was found to delete for Id = 1", deleteOutput.getRecordsWithErrors().get(0).getErrors().get(0));
deleteInput.setTableName(TestUtils.TABLE_NAME_ORDER_EXTRINSIC);
deleteInput.setPrimaryKeys(List.of(1));
deleteOutput = new DeleteAction().execute(deleteInput);
assertEquals(0, deleteOutput.getDeletedRecordCount());
assertEquals(1, deleteOutput.getRecordsWithErrors().size());
assertEquals("No record was found to delete for Id = 1", deleteOutput.getRecordsWithErrors().get(0).getErrors().get(0));
}
/*******************************************************************************
**
*******************************************************************************/
private void assertIdsExist(String tableName, List<Integer> ids) throws QException
{
List<QRecord> records = TestUtils.queryTable(tableName);
for(Integer id : ids)
{
assertTrue(records.stream().anyMatch(r -> Objects.equals(id, r.getValueInteger("id"))));
}
}
/*******************************************************************************
**
*******************************************************************************/
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

@ -25,6 +25,7 @@ package com.kingsrook.qqq.backend.core.actions.tables;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Objects;
import com.kingsrook.qqq.backend.core.BaseTest; import com.kingsrook.qqq.backend.core.BaseTest;
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;
@ -33,6 +34,7 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateOutput; import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateOutput;
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.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
import com.kingsrook.qqq.backend.core.utils.TestUtils; import com.kingsrook.qqq.backend.core.utils.TestUtils;
import com.kingsrook.qqq.backend.core.utils.collections.ListBuilder; import com.kingsrook.qqq.backend.core.utils.collections.ListBuilder;
import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder; import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder;
@ -393,237 +395,114 @@ class UpdateActionTest extends BaseTest
assertEquals("Missing value in required field: Order No", updateOutput.getRecords().get(3).getErrors().get(0)); assertEquals("Missing value in required field: Order No", updateOutput.getRecords().get(3).getErrors().get(0));
} }
/*******************************************************************************
**
*******************************************************************************/
/*
@Test
void testInsertMultiLevelSecurityJoins() throws QException
{
QContext.getQSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_TYPE_STORE, 1);
//////////////////////////////////////////////////////////////////////////////////////
// null value in the foreign key to the join-table that provides the security value //
//////////////////////////////////////////////////////////////////////////////////////
{
InsertInput insertInput = new InsertInput();
insertInput.setTableName(TestUtils.TABLE_NAME_LINE_ITEM_EXTRINSIC);
insertInput.setRecords(List.of(new QRecord().withValue("lineItemId", null).withValue("key", "kidsCanCallYou").withValue("value", "HoJu")));
InsertOutput insertOutput = new InsertAction().execute(insertInput);
assertEquals("You do not have permission to insert this record - the referenced Order was not found.", insertOutput.getRecords().get(0).getErrors().get(0));
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// value in the foreign key to the join-table that provides the security value, but the referenced record isn't found //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
{
InsertInput insertInput = new InsertInput();
insertInput.setTableName(TestUtils.TABLE_NAME_LINE_ITEM_EXTRINSIC);
insertInput.setRecords(List.of(new QRecord().withValue("lineItemId", 1701).withValue("key", "kidsCanCallYou").withValue("value", "HoJu")));
InsertOutput insertOutput = new InsertAction().execute(insertInput);
assertEquals("You do not have permission to insert this record - the referenced Order was not found.", insertOutput.getRecords().get(0).getErrors().get(0));
}
{
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// insert an order and lineItem with storeId=2 - then, reset our session to only have storeId=1 in it - and try to insert an order-line referencing that order. //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
QContext.getQSession().withSecurityKeyValues(new HashMap<>());
QContext.getQSession().withSecurityKeyValues(TestUtils.SECURITY_KEY_TYPE_STORE, List.of(2));
InsertInput insertOrderInput = new InsertInput();
insertOrderInput.setTableName(TestUtils.TABLE_NAME_ORDER);
insertOrderInput.setRecords(List.of(new QRecord().withValue("id", 42).withValue("storeId", 2)));
InsertOutput insertOrderOutput = new InsertAction().execute(insertOrderInput);
assertEquals(42, insertOrderOutput.getRecords().get(0).getValueInteger("id"));
InsertInput insertLineItemInput = new InsertInput();
insertLineItemInput.setTableName(TestUtils.TABLE_NAME_LINE_ITEM);
insertLineItemInput.setRecords(List.of(new QRecord().withValue("id", 4200).withValue("orderId", 42).withValue("sku", "BASIC1").withValue("quantity", 24)));
InsertOutput insertLineItemOutput = new InsertAction().execute(insertLineItemInput);
assertEquals(4200, insertLineItemOutput.getRecords().get(0).getValueInteger("id"));
QContext.getQSession().withSecurityKeyValues(new HashMap<>());
QContext.getQSession().withSecurityKeyValues(TestUtils.SECURITY_KEY_TYPE_STORE, List.of(1));
InsertInput insertLineItemExtrinsicInput = new InsertInput();
insertLineItemExtrinsicInput.setTableName(TestUtils.TABLE_NAME_LINE_ITEM_EXTRINSIC);
insertLineItemExtrinsicInput.setRecords(List.of(new QRecord().withValue("lineItemId", 4200).withValue("key", "kidsCanCallYou").withValue("value", "HoJu")));
InsertOutput insertLineItemExtrinsicOutput = new InsertAction().execute(insertLineItemExtrinsicInput);
assertEquals("You do not have permission to insert this record.", insertLineItemExtrinsicOutput.getRecords().get(0).getErrors().get(0));
}
{
QContext.getQSession().withSecurityKeyValues(new HashMap<>());
QContext.getQSession().withSecurityKeyValues(TestUtils.SECURITY_KEY_TYPE_STORE, List.of(1));
InsertInput insertOrderInput = new InsertInput();
insertOrderInput.setTableName(TestUtils.TABLE_NAME_ORDER);
insertOrderInput.setRecords(List.of(new QRecord().withValue("id", 47).withValue("storeId", 1)));
InsertOutput insertOrderOutput = new InsertAction().execute(insertOrderInput);
assertEquals(47, insertOrderOutput.getRecords().get(0).getValueInteger("id"));
InsertInput insertLineItemInput = new InsertInput();
insertLineItemInput.setTableName(TestUtils.TABLE_NAME_LINE_ITEM);
insertLineItemInput.setRecords(List.of(new QRecord().withValue("id", 4700).withValue("orderId", 47).withValue("sku", "BASIC1").withValue("quantity", 74)));
InsertOutput insertLineItemOutput = new InsertAction().execute(insertLineItemInput);
assertEquals(4700, insertLineItemOutput.getRecords().get(0).getValueInteger("id"));
///////////////////////////////////////////////////////
// combine all the above, plus one record that works //
///////////////////////////////////////////////////////
InsertInput insertInput = new InsertInput();
insertInput.setTableName(TestUtils.TABLE_NAME_LINE_ITEM_EXTRINSIC);
insertInput.setRecords(List.of(
new QRecord().withValue("lineItemId", null).withValue("key", "theKidsCanCallYou").withValue("value", "HoJu"),
new QRecord().withValue("lineItemId", 1701).withValue("key", "theKidsCanCallYou").withValue("value", "HoJu"),
new QRecord().withValue("lineItemId", 4200).withValue("key", "theKidsCanCallYou").withValue("value", "HoJu"),
new QRecord().withValue("lineItemId", 4700).withValue("key", "theKidsCanCallYou").withValue("value", "HoJu")
));
InsertOutput insertOutput = new InsertAction().execute(insertInput);
assertEquals("You do not have permission to insert this record - the referenced Order was not found.", insertOutput.getRecords().get(0).getErrors().get(0));
assertEquals("You do not have permission to insert this record - the referenced Order was not found.", insertOutput.getRecords().get(1).getErrors().get(0));
assertEquals("You do not have permission to insert this record.", insertOutput.getRecords().get(2).getErrors().get(0));
assertEquals(0, insertOutput.getRecords().get(3).getErrors().size());
assertNotNull(insertOutput.getRecords().get(3).getValueInteger("id"));
}
{
/////////////////////////////////////////////////////////////////////////////////
// one more time, but with multiple input records referencing each foreign key //
/////////////////////////////////////////////////////////////////////////////////
InsertInput insertInput = new InsertInput();
insertInput.setTableName(TestUtils.TABLE_NAME_LINE_ITEM_EXTRINSIC);
insertInput.setRecords(List.of(
new QRecord().withValue("lineItemId", null).withValue("key", "theKidsCanCallYou").withValue("value", "HoJu"),
new QRecord().withValue("lineItemId", 1701).withValue("key", "theKidsCanCallYou").withValue("value", "HoJu"),
new QRecord().withValue("lineItemId", 4200).withValue("key", "theKidsCanCallYou").withValue("value", "HoJu"),
new QRecord().withValue("lineItemId", 4700).withValue("key", "theKidsCanCallYou").withValue("value", "HoJu"),
new QRecord().withValue("lineItemId", null).withValue("key", "theKidsCanCallYou").withValue("value", "HoJu"),
new QRecord().withValue("lineItemId", 1701).withValue("key", "theKidsCanCallYou").withValue("value", "HoJu"),
new QRecord().withValue("lineItemId", 4200).withValue("key", "theKidsCanCallYou").withValue("value", "HoJu"),
new QRecord().withValue("lineItemId", 4700).withValue("key", "theKidsCanCallYou").withValue("value", "HoJu")
));
InsertOutput insertOutput = new InsertAction().execute(insertInput);
assertEquals("You do not have permission to insert this record - the referenced Order was not found.", insertOutput.getRecords().get(0).getErrors().get(0));
assertEquals("You do not have permission to insert this record - the referenced Order was not found.", insertOutput.getRecords().get(1).getErrors().get(0));
assertEquals("You do not have permission to insert this record.", insertOutput.getRecords().get(2).getErrors().get(0));
assertEquals(0, insertOutput.getRecords().get(3).getErrors().size());
assertNotNull(insertOutput.getRecords().get(3).getValueInteger("id"));
assertEquals("You do not have permission to insert this record - the referenced Order was not found.", insertOutput.getRecords().get(4).getErrors().get(0));
assertEquals("You do not have permission to insert this record - the referenced Order was not found.", insertOutput.getRecords().get(5).getErrors().get(0));
assertEquals("You do not have permission to insert this record.", insertOutput.getRecords().get(6).getErrors().get(0));
assertEquals(0, insertOutput.getRecords().get(7).getErrors().size());
assertNotNull(insertOutput.getRecords().get(7).getValueInteger("id"));
}
}
*/
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
/*
@Test @Test
void testInsertSingleLevelSecurityJoins() throws QException void testUpdateSecurityJoins() throws QException
{ {
QContext.getQSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_TYPE_STORE, 1); QContext.getQSession().setSecurityKeyValues(MapBuilder.of(TestUtils.SECURITY_KEY_TYPE_STORE, ListBuilder.of(1, 2)));
////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////
// null value in the foreign key to the join-table that provides the security value // // insert an order in each of store 1 and store 2 //
////////////////////////////////////////////////////////////////////////////////////// // with some lines and line-extrinsics //
{ ////////////////////////////////////////////////////
InsertInput insertInput = new InsertInput(); InsertInput insertInput = new InsertInput();
insertInput.setTableName(TestUtils.TABLE_NAME_LINE_ITEM); insertInput.setTableName(TestUtils.TABLE_NAME_ORDER);
insertInput.setRecords(List.of(new QRecord().withValue("orderId", null).withValue("sku", "BASIC1").withValue("quantity", 1))); insertInput.setRecords(List.of(
InsertOutput insertOutput = new InsertAction().execute(insertInput); new QRecord().withValue("id", 1).withValue("orderNo", "O1").withValue("storeId", 1),
assertEquals("You do not have permission to insert this record - the referenced Order was not found.", insertOutput.getRecords().get(0).getErrors().get(0)); new QRecord().withValue("id", 2).withValue("orderNo", "O2").withValue("storeId", 2)
} ));
new InsertAction().execute(insertInput);
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// value in the foreign key to the join-table that provides the security value, but the referenced record isn't found //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
{
InsertInput insertInput = new InsertInput();
insertInput.setTableName(TestUtils.TABLE_NAME_LINE_ITEM);
insertInput.setRecords(List.of(new QRecord().withValue("orderId", 1701).withValue("sku", "BASIC1").withValue("quantity", 1)));
InsertOutput insertOutput = new InsertAction().execute(insertInput);
assertEquals("You do not have permission to insert this record - the referenced Order was not found.", insertOutput.getRecords().get(0).getErrors().get(0));
}
{
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// insert an order with storeId=2 - then, reset our session to only have storeId=1 in it - and try to insert an order-line referencing that order. //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
QContext.getQSession().withSecurityKeyValues(new HashMap<>());
QContext.getQSession().withSecurityKeyValues(TestUtils.SECURITY_KEY_TYPE_STORE, List.of(2));
InsertInput insertOrderInput = new InsertInput();
insertOrderInput.setTableName(TestUtils.TABLE_NAME_ORDER);
insertOrderInput.setRecords(List.of(new QRecord().withValue("id", 42).withValue("storeId", 2)));
InsertOutput insertOrderOutput = new InsertAction().execute(insertOrderInput);
assertEquals(42, insertOrderOutput.getRecords().get(0).getValueInteger("id"));
QContext.getQSession().withSecurityKeyValues(new HashMap<>());
QContext.getQSession().withSecurityKeyValues(TestUtils.SECURITY_KEY_TYPE_STORE, List.of(1));
InsertInput insertLineItemInput = new InsertInput();
insertLineItemInput.setTableName(TestUtils.TABLE_NAME_LINE_ITEM);
insertLineItemInput.setRecords(List.of(new QRecord().withValue("orderId", 42).withValue("sku", "BASIC1").withValue("quantity", 1)));
InsertOutput insertLineItemOutput = new InsertAction().execute(insertLineItemInput);
assertEquals("You do not have permission to insert this record.", insertLineItemOutput.getRecords().get(0).getErrors().get(0));
}
{
QContext.getQSession().withSecurityKeyValues(new HashMap<>());
QContext.getQSession().withSecurityKeyValues(TestUtils.SECURITY_KEY_TYPE_STORE, List.of(1));
InsertInput insertOrderInput = new InsertInput();
insertOrderInput.setTableName(TestUtils.TABLE_NAME_ORDER);
insertOrderInput.setRecords(List.of(new QRecord().withValue("id", 47).withValue("storeId", 1)));
new InsertAction().execute(insertOrderInput);
///////////////////////////////////////////////////////
// combine all the above, plus one record that works //
///////////////////////////////////////////////////////
InsertInput insertInput = new InsertInput();
insertInput.setTableName(TestUtils.TABLE_NAME_LINE_ITEM); insertInput.setTableName(TestUtils.TABLE_NAME_LINE_ITEM);
insertInput.setRecords(List.of( insertInput.setRecords(List.of(
new QRecord().withValue("orderId", null).withValue("sku", "BASIC1").withValue("quantity", 1), new QRecord().withValue("id", 10).withValue("orderId", 1).withValue("sku", "BASIC1"),
new QRecord().withValue("orderId", 1701).withValue("sku", "BASIC1").withValue("quantity", 1), new QRecord().withValue("id", 20).withValue("orderId", 2).withValue("sku", "BASIC2")
new QRecord().withValue("orderId", 42).withValue("sku", "BASIC1").withValue("quantity", 1),
new QRecord().withValue("orderId", 47).withValue("sku", "BASIC1").withValue("quantity", 1)
)); ));
InsertOutput insertOutput = new InsertAction().execute(insertInput); new InsertAction().execute(insertInput);
assertEquals("You do not have permission to insert this record - the referenced Order was not found.", insertOutput.getRecords().get(0).getErrors().get(0));
assertEquals("You do not have permission to insert this record - the referenced Order was not found.", insertOutput.getRecords().get(1).getErrors().get(0)); insertInput.setTableName(TestUtils.TABLE_NAME_LINE_ITEM_EXTRINSIC);
assertEquals("You do not have permission to insert this record.", insertOutput.getRecords().get(2).getErrors().get(0)); insertInput.setRecords(List.of(
assertEquals(0, insertOutput.getRecords().get(3).getErrors().size()); new QRecord().withValue("id", 100).withValue("lineItemId", 10).withValue("key", "Key1").withValue("value", "Value1"),
assertNotNull(insertOutput.getRecords().get(3).getValueInteger("id")); new QRecord().withValue("id", 200).withValue("lineItemId", 20).withValue("key", "Key2").withValue("value", "Value2")
));
new InsertAction().execute(insertInput);
///////////////////////////////////////////////////////////
// try to remove the value that provides the foreign key //
///////////////////////////////////////////////////////////
{
UpdateInput updateInput = new UpdateInput();
updateInput.setTableName(TestUtils.TABLE_NAME_LINE_ITEM);
updateInput.setRecords(List.of(new QRecord().withValue("id", 10).withValue("orderId", null).withValue("sku", "BASIC2")));
UpdateOutput updateOutput = new UpdateAction().execute(updateInput);
assertEquals("You do not have permission to update this record - the referenced Order was not found.", updateOutput.getRecords().get(0).getErrors().get(0));
} }
////////////////////////////////////////////////////////////////////////////////////
// with a session that can only access store 1, try to update the line in store 2 //
// should fail as a not-found - you can't see that record. //
////////////////////////////////////////////////////////////////////////////////////
{ {
///////////////////////////////////////////////////////////////////////////////// QContext.getQSession().setSecurityKeyValues(MapBuilder.of(TestUtils.SECURITY_KEY_TYPE_STORE, ListBuilder.of(1)));
// one more time, but with multiple input records referencing each foreign key // UpdateInput updateInput = new UpdateInput();
///////////////////////////////////////////////////////////////////////////////// updateInput.setTableName(TestUtils.TABLE_NAME_LINE_ITEM);
InsertInput insertInput = new InsertInput(); updateInput.setRecords(List.of(new QRecord().withValue("id", 20).withValue("sku", "BASIC3")));
insertInput.setTableName(TestUtils.TABLE_NAME_LINE_ITEM); UpdateOutput updateOutput = new UpdateAction().execute(updateInput);
insertInput.setRecords(List.of( assertEquals("No record was found to update for Id = 20", updateOutput.getRecords().get(0).getErrors().get(0));
new QRecord().withValue("orderId", null).withValue("sku", "BASIC1").withValue("quantity", 1), }
new QRecord().withValue("orderId", 1701).withValue("sku", "BASIC1").withValue("quantity", 1),
new QRecord().withValue("orderId", 42).withValue("sku", "BASIC1").withValue("quantity", 1), ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
new QRecord().withValue("orderId", 47).withValue("sku", "BASIC1").withValue("quantity", 1), // with a session that can only access store 1, try to update the line from the order in store 1 to be in store 2 //
new QRecord().withValue("orderId", null).withValue("sku", "BASIC1").withValue("quantity", 1), ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
new QRecord().withValue("orderId", 1701).withValue("sku", "BASIC1").withValue("quantity", 1), {
new QRecord().withValue("orderId", 42).withValue("sku", "BASIC1").withValue("quantity", 1), QContext.getQSession().setSecurityKeyValues(MapBuilder.of(TestUtils.SECURITY_KEY_TYPE_STORE, ListBuilder.of(1)));
new QRecord().withValue("orderId", 47).withValue("sku", "BASIC1").withValue("quantity", 1) UpdateInput updateInput = new UpdateInput();
)); updateInput.setTableName(TestUtils.TABLE_NAME_LINE_ITEM);
InsertOutput insertOutput = new InsertAction().execute(insertInput); updateInput.setRecords(List.of(new QRecord().withValue("id", 10).withValue("orderId", 2).withValue("sku", "BASIC3")));
assertEquals("You do not have permission to insert this record - the referenced Order was not found.", insertOutput.getRecords().get(0).getErrors().get(0)); UpdateOutput updateOutput = new UpdateAction().execute(updateInput);
assertEquals("You do not have permission to insert this record - the referenced Order was not found.", insertOutput.getRecords().get(1).getErrors().get(0)); assertEquals("You do not have permission to update this record - the referenced Order was not found.", updateOutput.getRecords().get(0).getErrors().get(0));
assertEquals("You do not have permission to insert this record.", insertOutput.getRecords().get(2).getErrors().get(0)); }
assertEquals(0, insertOutput.getRecords().get(3).getErrors().size());
assertNotNull(insertOutput.getRecords().get(3).getValueInteger("id")); ///////////////////////////////////////////////////////////
assertEquals("You do not have permission to insert this record - the referenced Order was not found.", insertOutput.getRecords().get(4).getErrors().get(0)); // try to remove the value that provides the foreign key //
assertEquals("You do not have permission to insert this record - the referenced Order was not found.", insertOutput.getRecords().get(5).getErrors().get(0)); ///////////////////////////////////////////////////////////
assertEquals("You do not have permission to insert this record.", insertOutput.getRecords().get(6).getErrors().get(0)); {
assertEquals(0, insertOutput.getRecords().get(7).getErrors().size()); UpdateInput updateInput = new UpdateInput();
assertNotNull(insertOutput.getRecords().get(7).getValueInteger("id")); updateInput.setTableName(TestUtils.TABLE_NAME_LINE_ITEM_EXTRINSIC);
updateInput.setRecords(List.of(new QRecord().withValue("id", 100).withValue("lineItemId", null).withValue("key", "updatedKey")));
UpdateOutput updateOutput = new UpdateAction().execute(updateInput);
assertEquals("You do not have permission to update this record - the referenced Order was not found.", updateOutput.getRecords().get(0).getErrors().get(0));
}
//////////////////////////////////////////////////////////////////////////////////////////////
// with a session that can only access store 1, try to update the line-extrinsic in store 2 //
// should fail as a not-found - you can't see that record. //
//////////////////////////////////////////////////////////////////////////////////////////////
{
QContext.getQSession().setSecurityKeyValues(MapBuilder.of(TestUtils.SECURITY_KEY_TYPE_STORE, ListBuilder.of(1)));
UpdateInput updateInput = new UpdateInput();
updateInput.setTableName(TestUtils.TABLE_NAME_LINE_ITEM_EXTRINSIC);
updateInput.setRecords(List.of(new QRecord().withValue("id", 200).withValue("key", "updatedKey")));
UpdateOutput updateOutput = new UpdateAction().execute(updateInput);
assertEquals("No record was found to update for Id = 200", updateOutput.getRecords().get(0).getErrors().get(0));
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// with a session that can only access store 1, try to update the line-extrinsic from the order in store 1 to be in store 2 //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
{
QContext.getQSession().setSecurityKeyValues(MapBuilder.of(TestUtils.SECURITY_KEY_TYPE_STORE, ListBuilder.of(1)));
UpdateInput updateInput = new UpdateInput();
updateInput.setTableName(TestUtils.TABLE_NAME_LINE_ITEM_EXTRINSIC);
updateInput.setRecords(List.of(new QRecord().withValue("id", 100).withValue("lineItemId", 20).withValue("key", "updatedKey")));
UpdateOutput updateOutput = new UpdateAction().execute(updateInput);
assertEquals("You do not have permission to update this record - the referenced Order was not found.", updateOutput.getRecords().get(0).getErrors().get(0));
} }
} }
*/
@ -690,62 +569,101 @@ class UpdateActionTest extends BaseTest
assertThat(TestUtils.queryTable(TestUtils.TABLE_NAME_ORDER)).anyMatch(r -> r.getValueString("orderNo").equals("original")); assertThat(TestUtils.queryTable(TestUtils.TABLE_NAME_ORDER)).anyMatch(r -> r.getValueString("orderNo").equals("original"));
} }
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
/*
@Test @Test
void testSecurityKeyNullDenied() throws QException void testSecurityKeyNullDenied() throws QException
{ {
QInstance qInstance = QContext.getQInstance(); ////////////////////////////////
// insert an order in store 1 //
////////////////////////////////
QContext.getQSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_TYPE_STORE, 1); QContext.getQSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_TYPE_STORE, 1);
InsertInput insertInput = new InsertInput(); InsertInput insertInput = new InsertInput();
insertInput.setTableName(TestUtils.TABLE_NAME_ORDER); insertInput.setTableName(TestUtils.TABLE_NAME_ORDER);
insertInput.setRecords(List.of(new QRecord())); insertInput.setRecords(List.of(new QRecord().withValue("id", 1).withValue("orderNo", "original").withValue("storeId", 1)));
InsertOutput insertOutput = new InsertAction().execute(insertInput); new InsertAction().execute(insertInput);
assertEquals("You do not have permission to insert a record without a value in the field: Store Id", insertOutput.getRecords().get(0).getErrors().get(0));
assertEquals(0, TestUtils.queryTable(TestUtils.TABLE_NAME_ORDER).size()); ///////////////////////////////////////////
// try to update its storeId to null now //
///////////////////////////////////////////
UpdateInput updateInput = new UpdateInput();
updateInput.setTableName(TestUtils.TABLE_NAME_ORDER);
updateInput.setRecords(List.of(new QRecord().withValue("id", 1).withValue("storeId", null)));
UpdateOutput updateOutput = new UpdateAction().execute(updateInput);
assertEquals("You do not have permission to update a record without a value in the field: Store Id", updateOutput.getRecords().get(0).getErrors().get(0));
assertEquals(0, TestUtils.queryTable(TestUtils.TABLE_NAME_ORDER).stream().filter(r -> r.getValue("storeId") == null).count());
} }
*/
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
/*
@Test @Test
void testSecurityKeyNullAllowed() throws QException void testSecurityKeyNullAllowed() throws QException
{ {
/////////////////////////////////////
// change storeId to be allow-null //
/////////////////////////////////////
QInstance qInstance = QContext.getQInstance(); QInstance qInstance = QContext.getQInstance();
qInstance.getTable(TestUtils.TABLE_NAME_ORDER).getRecordSecurityLocks().get(0).setNullValueBehavior(RecordSecurityLock.NullValueBehavior.ALLOW); qInstance.getTable(TestUtils.TABLE_NAME_ORDER).getRecordSecurityLocks().get(0).setNullValueBehavior(RecordSecurityLock.NullValueBehavior.ALLOW);
////////////////////////////////
// insert an order in store 1 //
////////////////////////////////
QContext.getQSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_TYPE_STORE, 1); QContext.getQSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_TYPE_STORE, 1);
InsertInput insertInput = new InsertInput(); InsertInput insertInput = new InsertInput();
insertInput.setTableName(TestUtils.TABLE_NAME_ORDER); insertInput.setTableName(TestUtils.TABLE_NAME_ORDER);
insertInput.setRecords(List.of(new QRecord())); insertInput.setRecords(List.of(new QRecord().withValue("id", 1).withValue("orderNo", "original").withValue("storeId", 1)));
InsertOutput insertOutput = new InsertAction().execute(insertInput); new InsertAction().execute(insertInput);
assertEquals(0, insertOutput.getRecords().get(0).getErrors().size());
assertEquals(1, TestUtils.queryTable(TestUtils.TABLE_NAME_ORDER).size()); QContext.getQSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_TYPE_STORE, 1);
UpdateInput updateInput = new UpdateInput();
updateInput.setTableName(TestUtils.TABLE_NAME_ORDER);
updateInput.setRecords(List.of(new QRecord().withValue("id", 1).withValue("storeId", null)));
UpdateOutput updateOutput = new UpdateAction().execute(updateInput);
assertEquals(0, updateOutput.getRecords().get(0).getErrors().size());
assertEquals(1, TestUtils.queryTable(TestUtils.TABLE_NAME_ORDER).stream().filter(r -> r.getValue("storeId") == null).count());
} }
*/
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
/*
@Test @Test
void testSecurityKeyAllAccess() throws QException void testSecurityKeyAllAccess() throws QException
{ {
QInstance qInstance = QContext.getQInstance(); ////////////////////////////////
qInstance.getTable(TestUtils.TABLE_NAME_ORDER).getRecordSecurityLocks().get(0).setNullValueBehavior(RecordSecurityLock.NullValueBehavior.ALLOW); // insert 2 orders in store 1 //
QContext.getQSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_TYPE_STORE_ALL_ACCESS, true); ////////////////////////////////
QContext.getQSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_TYPE_STORE, 1);
InsertInput insertInput = new InsertInput(); InsertInput insertInput = new InsertInput();
insertInput.setTableName(TestUtils.TABLE_NAME_ORDER); insertInput.setTableName(TestUtils.TABLE_NAME_ORDER);
insertInput.setRecords(List.of( insertInput.setRecords(List.of(
new QRecord().withValue("storeId", 999), new QRecord().withValue("id", 1).withValue("orderNo", "O1").withValue("storeId", 1),
new QRecord().withValue("storeId", null) new QRecord().withValue("id", 2).withValue("orderNo", "O2").withValue("storeId", 1)
)); ));
InsertOutput insertOutput = new InsertAction().execute(insertInput); new InsertAction().execute(insertInput);
assertEquals(2, TestUtils.queryTable(TestUtils.TABLE_NAME_ORDER).size());
/////////////////////////////////////////////////////////
// make sure with all-access key we can update however //
/////////////////////////////////////////////////////////
QContext.getQSession().setSecurityKeyValues(MapBuilder.of(TestUtils.SECURITY_KEY_TYPE_STORE_ALL_ACCESS, ListBuilder.of(true)));
UpdateInput updateInput = new UpdateInput();
updateInput.setTableName(TestUtils.TABLE_NAME_ORDER);
updateInput.setRecords(List.of(
new QRecord().withValue("id", 1).withValue("storeId", 999),
new QRecord().withValue("id", 2).withValue("storeId", null)
));
UpdateOutput updateOutput = new UpdateAction().execute(updateInput);
assertEquals(0, updateOutput.getRecords().get(0).getErrors().size());
assertEquals(0, updateOutput.getRecords().get(1).getErrors().size());
assertEquals(1, TestUtils.queryTable(TestUtils.TABLE_NAME_ORDER).stream().filter(r -> Objects.equals(r.getValue("storeId"), 999)).count());
assertEquals(1, TestUtils.queryTable(TestUtils.TABLE_NAME_ORDER).stream().filter(r -> r.getValue("storeId") == null).count());
} }
*/
} }

View File

@ -25,6 +25,7 @@ package com.kingsrook.qqq.backend.core.modules.backend.implementations.utils;
import java.util.List; import java.util.List;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.utils.collections.ListBuilder;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
@ -97,6 +98,21 @@ class BackendQueryFilterUtilsTest
assertTrue(BackendQueryFilterUtils.doesCriteriaMatch(new QFilterCriteria("f", QCriteriaOperator.NOT_LIKE, "Test"), "f", "Tst")); assertTrue(BackendQueryFilterUtils.doesCriteriaMatch(new QFilterCriteria("f", QCriteriaOperator.NOT_LIKE, "Test"), "f", "Tst"));
assertTrue(BackendQueryFilterUtils.doesCriteriaMatch(new QFilterCriteria("f", QCriteriaOperator.NOT_LIKE, "T%"), "f", "Rest")); assertTrue(BackendQueryFilterUtils.doesCriteriaMatch(new QFilterCriteria("f", QCriteriaOperator.NOT_LIKE, "T%"), "f", "Rest"));
assertTrue(BackendQueryFilterUtils.doesCriteriaMatch(new QFilterCriteria("f", QCriteriaOperator.NOT_LIKE, "T_st"), "f", "Toast")); assertTrue(BackendQueryFilterUtils.doesCriteriaMatch(new QFilterCriteria("f", QCriteriaOperator.NOT_LIKE, "T_st"), "f", "Toast"));
//////////////
// IN & NOT //
//////////////
assertTrue(BackendQueryFilterUtils.doesCriteriaMatch(new QFilterCriteria("f", QCriteriaOperator.IN, "A"), "f", "A"));
assertTrue(BackendQueryFilterUtils.doesCriteriaMatch(new QFilterCriteria("f", QCriteriaOperator.IN, "A", "B"), "f", "A"));
assertTrue(BackendQueryFilterUtils.doesCriteriaMatch(new QFilterCriteria("f", QCriteriaOperator.IN, "A", "B"), "f", "B"));
assertFalse(BackendQueryFilterUtils.doesCriteriaMatch(new QFilterCriteria("f", QCriteriaOperator.IN, List.of()), "f", "A"));
assertFalse(BackendQueryFilterUtils.doesCriteriaMatch(new QFilterCriteria("f", QCriteriaOperator.IN, ListBuilder.of(null)), "f", "A"));
assertFalse(BackendQueryFilterUtils.doesCriteriaMatch(new QFilterCriteria("f", QCriteriaOperator.NOT_IN, "A"), "f", "A"));
assertFalse(BackendQueryFilterUtils.doesCriteriaMatch(new QFilterCriteria("f", QCriteriaOperator.NOT_IN, "A", "B"), "f", "A"));
assertFalse(BackendQueryFilterUtils.doesCriteriaMatch(new QFilterCriteria("f", QCriteriaOperator.NOT_IN, "A", "B"), "f", "B"));
assertTrue(BackendQueryFilterUtils.doesCriteriaMatch(new QFilterCriteria("f", QCriteriaOperator.NOT_IN, List.of()), "f", "A"));
assertTrue(BackendQueryFilterUtils.doesCriteriaMatch(new QFilterCriteria("f", QCriteriaOperator.NOT_IN, ListBuilder.of(null)), "f", "A"));
} }

View File

@ -619,6 +619,10 @@ public class TestUtils
.withName(TABLE_NAME_ORDER_EXTRINSIC) .withName(TABLE_NAME_ORDER_EXTRINSIC)
.withBackendName(MEMORY_BACKEND_NAME) .withBackendName(MEMORY_BACKEND_NAME)
.withPrimaryKeyField("id") .withPrimaryKeyField("id")
.withRecordSecurityLock(new RecordSecurityLock()
.withSecurityKeyType(SECURITY_KEY_TYPE_STORE)
.withFieldName("order.storeId")
.withJoinNameChain(List.of("orderOrderExtrinsic")))
.withField(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false)) .withField(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false))
.withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME).withIsEditable(false)) .withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME).withIsEditable(false))
.withField(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME).withIsEditable(false)) .withField(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME).withIsEditable(false))

View File

@ -23,8 +23,10 @@ package com.kingsrook.qqq.backend.core.utils.collections;
import java.util.Map; import java.util.Map;
import java.util.TreeMap;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
/******************************************************************************* /*******************************************************************************
@ -50,6 +52,7 @@ class MapBuilderTest
} }
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
@ -61,9 +64,22 @@ class MapBuilderTest
/////////////////////////////// ///////////////////////////////
Map<String, Object> map = MapBuilder.of("1", null); Map<String, Object> map = MapBuilder.of("1", null);
///////////////////////////////////////
// this too, doesn't freaking throw. // // this too, doesn't freaking throw. //
/////////////////////////////////////// ///////////////////////////////////////
map.put("2", null); map.put("2", null);
} }
/*******************************************************************************
**
*******************************************************************************/
@Test
void testTypeYouRequest()
{
Map<String, Integer> myTreeMap = MapBuilder.<String, Integer>of(TreeMap::new).with("1", 1).with("2", 2).build();
assertTrue(myTreeMap instanceof TreeMap);
}
} }

View File

@ -92,6 +92,95 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
{ {
private static final QLogger LOG = QLogger.getLogger(GenerateOpenApiSpecAction.class); private static final QLogger LOG = QLogger.getLogger(GenerateOpenApiSpecAction.class);
public static final String GET_DESCRIPTION = """
Get one record from this table, by specifying its primary key as a path parameter.
""";
public static final String QUERY_DESCRIPTION = """
Execute a query on this table, using query criteria as specified in query string parameters.
* Pagination is managed via the `pageNo` & `pageSize` query string parameters. pageNo starts at 1. pageSize defaults to 50.
* By default, the response includes the total count of records that match the query criteria. The count can be omitted by specifying `includeCount=false`
* By default, results are sorted by the table's primary key, descending. This can be changed by specifying the `orderBy` query string parameter, following SQL ORDER BY syntax (e.g., `fieldName1 ASC, fieldName2 DESC`)
* By default, all given query criteria are combined using logical AND. This can be changed by specifying the query string parameter `booleanOperator=OR`.
* Each field on the table can be used as a query criteria. Each query criteria field can be specified on the query string any number of times.
* By default, all criteria use the equals operator (e.g., `myField=value` means records will be returned where myField equals value). Alternative operators can be used as follows:
* Equals: `myField=value`
* Not Equals: `myField=!value`
* Less Than: `myField=&lt;value`
* Greater Than: `myField=&gt;value`
* Less Than or Equals: `myField=&lt;=value`
* Greater Than or Equals: `myField=&gt;=value`
* Empty (or null): `myField=EMPTY`
* Not Empty: `myField=!EMPTY`
* Between: `myField=BETWEEN value1,value2` (two values must be given, separated by commas)
* Not Between: `myField=!BETWEEN value1,value2` (two values must be given, separated by commas)
* In: `myField=IN value1,value2,...,valueN` (one or more values must be given, separated by commas)
* Not In: `myField=!IN value1,value2,...,valueN` (one or more values must be given, separated by commas)
* Like: `myField=LIKE value` (using standard SQL % and _ wildcards)
* Not Like: `myField=!LIKE value` (using standard SQL % and _ wildcards)
""";
public static final String INSERT_DESCRIPTION = """
Insert one record into this table by supplying the values to be inserted in the request body.
* The request body should not include a value for the table's primary key. Rather, a value will be generated and returned in a successful response's body.
* Any unrecognized field names in the body will cause a 400 error.
* Any read-only (non-editable) fields provided in the body will be silently ignored.
Upon success, a status code of 201 (`Created`) is returned, and the generated value for the primary key will be returned in the response body object.
""";
public static final String UPDATE_DESCRIPTION = """
Update one record in this table, by specifying its primary key as a path parameter, and by supplying values to be updated in the request body.
* Only the fields provided in the request body will be updated.
* To remove a value from a field, supply the key for the field, with a null value.
* The request body does not need to contain all fields from the table. Rather, only the fields to be updated should be supplied.
* Any unrecognized field names in the body will cause a 400 error.
* Any read-only (non-editable) fields provided in the body will be silently ignored.
* Note that if the request body includes the primary key, it will be ignored. Only the primary key value path parameter will be used.
Upon success, a status code of 204 (`No Content`) is returned, with no response body.
""";
public static final String DELETE_DESCRIPTION = """
Delete one record from this table, by specifying its primary key as a path parameter.
Upon success, a status code of 204 (`No Content`) is returned, with no response body.
""";
public static final String BULK_INSERT_DESCRIPTION = """
Insert one or more records into this table by supplying array of records with values to be inserted, in the request body.
* The objects in the request body should not include a value for the table's primary key. Rather, a value will be generated and returned in a successful response's body
* Any unrecognized field names in the body will cause a 400 error.
* Any read-only (non-editable) fields provided in the body will be silently ignored.
An HTTP 207 (`Multi-Status`) code is generally returned, with an array of objects giving the individual sub-status codes for each record in the request body.
* The 1st record in the request will have its response in the 1st object in the response, and so-forth.
* For sub-status codes of 201 (`Created`), and the generated value for the primary key will be returned in the response body object.
""";
public static final String BULK_UPDATE_DESCRIPTION = """
Update one or more records in this table, by supplying an array of records, with primary keys and values to be updated, in the request body.
* Only the fields provided in the request body will be updated.
* To remove a value from a field, supply the key for the field, with a null value.
* The request body does not need to contain all fields from the table. Rather, only the fields to be updated should be supplied.
* Any unrecognized field names in the body will cause a 400 error.
* Any read-only (non-editable) fields provided in the body will be silently ignored.
An HTTP 207 (`Multi-Status`) code is generally returned, with an array of objects giving the individual sub-status codes for each record in the request body.
* The 1st record in the request will have its response in the 1st object in the response, and so-forth.
* Each input object's primary key will also be included in the corresponding response object.
""";
public static final String BULK_DELETE_DESCRIPTION = """
Delete one or more records from this table, by supplying an array of primary key values in the request body.
An HTTP 207 (`Multi-Status`) code is generally returned, with an array of objects giving the individual sub-status codes for each record in the request body.
* The 1st primary key in the request will have its response in the 1st object in the response, and so-forth.
* Each input primary key will also be included in the corresponding response object.
""";
/******************************************************************************* /*******************************************************************************
@ -144,6 +233,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
LinkedHashMap<String, String> scopes = new LinkedHashMap<>(); LinkedHashMap<String, String> scopes = new LinkedHashMap<>();
// todo, or not todo? .withScopes(scopes) // todo, or not todo? .withScopes(scopes)
// seems to make a lot of "noise" on the Auth page, and for no obvious benefit...
securitySchemes.put("OAuth2", new OAuth2() securitySchemes.put("OAuth2", new OAuth2()
.withFlows(MapBuilder.of("clientCredentials", new OAuth2Flow() .withFlows(MapBuilder.of("clientCredentials", new OAuth2Flow()
.withTokenUrl("/api/oauth/token")))); .withTokenUrl("/api/oauth/token"))));
@ -297,35 +387,20 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
.withAllOf(ListBuilder.of( .withAllOf(ListBuilder.of(
new Schema().withRef("#/components/schemas/" + tableApiName))))))); new Schema().withRef("#/components/schemas/" + tableApiName)))))));
// todo...?
// includeAssociatedOrderLines=false&includeAssociatedExtrinsics=false&includeAssociatedOrderLinesExtrinsics
// includeAssociatedRecords=none
// includeAssociatedRecords=all
// includeAssociatedRecords=orderLines
// includeAssociatedRecords=orderLines,orderLines.extrinsics
// includeAssociatedRecords=extrinsics,orderLines,orderLines.extrinsics
////////////////////////////////////// //////////////////////////////////////
// paths and methods for this table // // paths and methods for this table //
////////////////////////////////////// //////////////////////////////////////
Method queryGet = new Method() Method queryGet = new Method()
.withSummary("Search for " + tableLabel + " records by query string") .withSummary("Search for " + tableLabel + " records by query string")
.withDescription(""" .withDescription(QUERY_DESCRIPTION)
Execute a query on this table, using query criteria as specified in query string parameters.
* Pagination is managed via the `pageNo` & `pageSize` query string parameters. pageNo starts at 1. pageSize defaults to 50.
* By default, the response includes the total count of records that match the query criteria. The count can be omitted by specifying `includeCount=false`
* By default, results are sorted by the table's primary key, descending. This can be changed by specifying the `orderBy` query string parameter, following SQL ORDER BY syntax (e.g., `fieldName1 ASC, fieldName2 DESC`)
* By default, all given query criteria are combined using logical AND. This can be changed by specifying the query string parameter `booleanOperator=OR`.
* Each field on the table can be used as a query criteria. Each query criteria field can be specified on the query string any number of times.
* By default, all criteria use the equals operator (e.g., `myField=value` means records will be returned where myField equals value). Alternative operators can be used as follows:
* Equals: `myField=value`
* Not Equals: `myField=!value`
* Less Than: `myField=&lt;value`
* Greater Than: `myField=&gt;value`
* Less Than or Equals: `myField=&lt;=value`
* Greater Than or Equals: `myField=&gt;=value`
* Empty (or null): `myField=EMPTY`
* Not Empty: `myField=!EMPTY`
* Between: `myField=BETWEEN value1,value2` (two values must be given, separated by commas)
* Not Between: `myField=!BETWEEN value1,value2` (two values must be given, separated by commas)
* In: `myField=IN value1,value2,...,valueN` (one or more values must be given, separated by commas)
* Not In: `myField=!IN value1,value2,...,valueN` (one or more values must be given, separated by commas)
* Like: `myField=LIKE value` (using standard SQL % and _ wildcards)
* Not Like: `myField=!LIKE value` (using standard SQL % and _ wildcards)
""")
.withOperationId("query" + tableApiNameUcFirst) .withOperationId("query" + tableApiNameUcFirst)
.withTags(ListBuilder.of(tableLabel)) .withTags(ListBuilder.of(tableLabel))
.withParameters(ListBuilder.of( .withParameters(ListBuilder.of(
@ -390,9 +465,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
Method idGet = new Method() Method idGet = new Method()
.withSummary("Get one " + tableLabel + " by " + primaryKeyLabel) .withSummary("Get one " + tableLabel + " by " + primaryKeyLabel)
.withDescription(""" .withDescription(GET_DESCRIPTION)
Get one record from this table, by specifying its primary key as a path parameter.
""")
.withOperationId("get" + tableApiNameUcFirst) .withOperationId("get" + tableApiNameUcFirst)
.withTags(ListBuilder.of(tableLabel)) .withTags(ListBuilder.of(tableLabel))
.withParameters(ListBuilder.of( .withParameters(ListBuilder.of(
@ -412,16 +485,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
Method idPatch = new Method() Method idPatch = new Method()
.withSummary("Update one " + tableLabel) .withSummary("Update one " + tableLabel)
.withDescription(""" .withDescription(UPDATE_DESCRIPTION)
Update one record in this table, by specifying its primary key as a path parameter, and by supplying values to be updated in the request body.
* Only the fields provided in the request body will be updated.
* To remove a value from a field, supply the key for the field, with a null value.
* The request body does not need to contain all fields from the table. Rather, only the fields to be updated should be supplied.
* Note that if the request body includes the primary key, it will be ignored. Only the primary key value path parameter will be used.
Upon success, a status code of 204 (`No Content`) is returned, with no response body.
""")
.withOperationId("update" + tableApiNameUcFirst) .withOperationId("update" + tableApiNameUcFirst)
.withTags(ListBuilder.of(tableLabel)) .withTags(ListBuilder.of(tableLabel))
.withParameters(ListBuilder.of( .withParameters(ListBuilder.of(
@ -443,11 +507,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
Method idDelete = new Method() Method idDelete = new Method()
.withSummary("Delete one " + tableLabel) .withSummary("Delete one " + tableLabel)
.withDescription(""" .withDescription(DELETE_DESCRIPTION)
Delete one record from this table, by specifying its primary key as a path parameter.
Upon success, a status code of 204 (`No Content`) is returned, with no response body.
""")
.withOperationId("delete" + tableApiNameUcFirst) .withOperationId("delete" + tableApiNameUcFirst)
.withTags(ListBuilder.of(tableLabel)) .withTags(ListBuilder.of(tableLabel))
.withParameters(ListBuilder.of( .withParameters(ListBuilder.of(
@ -473,12 +533,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
Method slashPost = new Method() Method slashPost = new Method()
.withSummary("Create one " + tableLabel) .withSummary("Create one " + tableLabel)
.withDescription(""" .withDescription(INSERT_DESCRIPTION)
Insert one record into this table by supplying the values to be inserted in the request body.
* The request body should not include a value for the table's primary key. Rather, a value will be generated and returned in a successful response's body.
Upon success, a status code of 201 (`Created`) is returned, and the generated value for the primary key will be returned in the response body object.
""")
.withRequestBody(new RequestBody() .withRequestBody(new RequestBody()
.withRequired(true) .withRequired(true)
.withDescription("Values for the " + tableLabel + " record to create.") .withDescription("Values for the " + tableLabel + " record to create.")
@ -508,14 +563,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
//////////////// ////////////////
Method bulkPost = new Method() Method bulkPost = new Method()
.withSummary("Create multiple " + tableLabel + " records") .withSummary("Create multiple " + tableLabel + " records")
.withDescription(""" .withDescription(BULK_INSERT_DESCRIPTION)
Insert one or more records into this table by supplying array of records with values to be inserted, in the request body.
* The objects in the request body should not include a value for the table's primary key. Rather, a value will be generated and returned in a successful response's body
An HTTP 207 (`Multi-Status`) code is generally returned, with an array of objects giving the individual sub-status codes for each record in the request body.
* The 1st record in the request will have its response in the 1st object in the response, and so-forth.
* For sub-status codes of 201 (`Created`), and the generated value for the primary key will be returned in the response body object.
""")
.withRequestBody(new RequestBody() .withRequestBody(new RequestBody()
.withRequired(true) .withRequired(true)
.withDescription("Values for the " + tableLabel + " records to create.") .withDescription("Values for the " + tableLabel + " records to create.")
@ -530,15 +578,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
Method bulkPatch = new Method() Method bulkPatch = new Method()
.withSummary("Update multiple " + tableLabel + " records") .withSummary("Update multiple " + tableLabel + " records")
.withDescription(""" .withDescription(BULK_UPDATE_DESCRIPTION)
Update one or more records in this table, by supplying an array of records, with primary keys and values to be updated, in the request body.
* Only the fields provided in the request body will be updated.
* To remove a value from a field, supply the key for the field, with a null value.
* The request body does not need to contain all fields from the table. Rather, only the fields to be updated should be supplied.
An HTTP 207 (`Multi-Status`) code is generally returned, with an array of objects giving the individual sub-status codes for each record in the request body.
* The 1st record in the request will have its response in the 1st object in the response, and so-forth.
""")
.withRequestBody(new RequestBody() .withRequestBody(new RequestBody()
.withRequired(true) .withRequired(true)
.withDescription("Values for the " + tableLabel + " records to update.") .withDescription("Values for the " + tableLabel + " records to update.")
@ -559,12 +599,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
Method bulkDelete = new Method() Method bulkDelete = new Method()
.withSummary("Delete multiple " + tableLabel + " records") .withSummary("Delete multiple " + tableLabel + " records")
.withDescription(""" .withDescription(BULK_DELETE_DESCRIPTION)
Delete one or more records from this table, by supplying an array of primary key values in the request body.
An HTTP 207 (`Multi-Status`) code is generally returned, with an array of objects giving the individual sub-status codes for each record in the request body.
* The 1st primary key in the request will have its response in the 1st object in the response, and so-forth.
""")
.withRequestBody(new RequestBody() .withRequestBody(new RequestBody()
.withRequired(true) .withRequired(true)
.withDescription(primaryKeyLabel + " values for the " + tableLabel + " records to delete.") .withDescription(primaryKeyLabel + " values for the " + tableLabel + " records to delete.")
@ -835,20 +870,34 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
case "patch" -> ListBuilder.of( case "patch" -> ListBuilder.of(
MapBuilder.of(LinkedHashMap::new) MapBuilder.of(LinkedHashMap::new)
.with("statusCode", HttpStatus.NO_CONTENT.getCode()) .with("statusCode", HttpStatus.NO_CONTENT.getCode())
.with("statusText", HttpStatus.NO_CONTENT.getMessage()).build(), .with("statusText", HttpStatus.NO_CONTENT.getMessage())
.with(primaryKeyApiName, "47").build(),
MapBuilder.of(LinkedHashMap::new) MapBuilder.of(LinkedHashMap::new)
.with("statusCode", HttpStatus.BAD_REQUEST.getCode()) .with("statusCode", HttpStatus.BAD_REQUEST.getCode())
.with("statusText", HttpStatus.BAD_REQUEST.getMessage()) .with("statusText", HttpStatus.BAD_REQUEST.getMessage())
.with("error", "Could not update " + tableLabel + ": Duplicate value in unique key field.").build() .with("error", "Could not update " + tableLabel + ": Missing value in required field: My Field.")
.with(primaryKeyApiName, "47").build(),
MapBuilder.of(LinkedHashMap::new)
.with("statusCode", HttpStatus.NOT_FOUND.getCode())
.with("statusText", HttpStatus.NOT_FOUND.getMessage())
.with("error", "The requested " + tableLabel + " to update was not found.")
.with(primaryKeyApiName, "47").build()
); );
case "delete" -> ListBuilder.of( case "delete" -> ListBuilder.of(
MapBuilder.of(LinkedHashMap::new) MapBuilder.of(LinkedHashMap::new)
.with("statusCode", HttpStatus.NO_CONTENT.getCode()) .with("statusCode", HttpStatus.NO_CONTENT.getCode())
.with("statusText", HttpStatus.NO_CONTENT.getMessage()).build(), .with("statusText", HttpStatus.NO_CONTENT.getMessage())
.with(primaryKeyApiName, "47").build(),
MapBuilder.of(LinkedHashMap::new) MapBuilder.of(LinkedHashMap::new)
.with("statusCode", HttpStatus.BAD_REQUEST.getCode()) .with("statusCode", HttpStatus.BAD_REQUEST.getCode())
.with("statusText", HttpStatus.BAD_REQUEST.getMessage()) .with("statusText", HttpStatus.BAD_REQUEST.getMessage())
.with("error", "Could not delete " + tableLabel + ": Foreign key constraint violation.").build() .with("error", "Could not delete " + tableLabel + ": Foreign key constraint violation.")
.with(primaryKeyApiName, "47").build(),
MapBuilder.of(LinkedHashMap::new)
.with("statusCode", HttpStatus.NOT_FOUND.getCode())
.with("statusText", HttpStatus.NOT_FOUND.getMessage())
.with("error", "The requested " + tableLabel + " to delete was not found.")
.with(primaryKeyApiName, "47").build()
); );
default -> throw (new IllegalArgumentException("Unrecognized method: " + method)); default -> throw (new IllegalArgumentException("Unrecognized method: " + method));
}; };
@ -857,10 +906,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
properties.put("statusCode", new Schema().withType("integer")); properties.put("statusCode", new Schema().withType("integer"));
properties.put("statusText", new Schema().withType("string")); properties.put("statusText", new Schema().withType("string"));
properties.put("error", new Schema().withType("string")); properties.put("error", new Schema().withType("string"));
if(method.equalsIgnoreCase("post"))
{
properties.put(primaryKeyApiName, new Schema().withType(getFieldType(primaryKeyField))); properties.put(primaryKeyApiName, new Schema().withType(getFieldType(primaryKeyField)));
}
return new Response() return new Response()
.withDescription("Multiple statuses. See body for details.") .withDescription("Multiple statuses. See body for details.")
@ -870,9 +916,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
.withItems(new Schema() .withItems(new Schema()
.withType("object") .withType("object")
.withProperties(properties)) .withProperties(properties))
.withExample(example) .withExample(example))));
)
));
} }

View File

@ -34,6 +34,7 @@ import com.kingsrook.qqq.api.model.actions.GetTableApiFieldsInput;
import com.kingsrook.qqq.api.model.metadata.fields.ApiFieldMetaData; import com.kingsrook.qqq.api.model.metadata.fields.ApiFieldMetaData;
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.logging.QLogger;
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.fields.QFieldMetaData; 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.Association;
@ -50,6 +51,8 @@ import org.json.JSONObject;
*******************************************************************************/ *******************************************************************************/
public class QRecordApiAdapter public class QRecordApiAdapter
{ {
private static final QLogger LOG = QLogger.getLogger(QRecordApiAdapter.class);
private static Map<Pair<String, String>, List<QFieldMetaData>> fieldListCache = new HashMap<>(); private static Map<Pair<String, String>, List<QFieldMetaData>> fieldListCache = new HashMap<>();
private static Map<Pair<String, String>, Map<String, QFieldMetaData>> fieldMapCache = new HashMap<>(); private static Map<Pair<String, String>, Map<String, QFieldMetaData>> fieldMapCache = new HashMap<>();
@ -105,7 +108,7 @@ public class QRecordApiAdapter
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
public static QRecord apiJsonObjectToQRecord(JSONObject jsonObject, String tableName, String apiVersion) throws QException public static QRecord apiJsonObjectToQRecord(JSONObject jsonObject, String tableName, String apiVersion, boolean includePrimaryKey) throws QException
{ {
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
// make map of apiFieldNames (e.g., names as api uses them) to QFieldMetaData // // make map of apiFieldNames (e.g., names as api uses them) to QFieldMetaData //
@ -134,6 +137,22 @@ public class QRecordApiAdapter
QFieldMetaData field = apiFieldsMap.get(jsonKey); QFieldMetaData field = apiFieldsMap.get(jsonKey);
Object value = jsonObject.isNull(jsonKey) ? null : jsonObject.get(jsonKey); Object value = jsonObject.isNull(jsonKey) ? null : jsonObject.get(jsonKey);
////////////////////////////////////////////////////////////////////////////////////////////////////////
// generally, omit non-editable fields - //
// however - if we're asked to include the primary key (and this is the primary key), then include it //
////////////////////////////////////////////////////////////////////////////////////////////////////////
if(!field.getIsEditable())
{
if(includePrimaryKey && field.getName().equals(table.getPrimaryKeyField()))
{
LOG.trace("Even though field [" + field.getName() + "] is not editable, we'll use it, because it's the primary key, and we've been asked to include primary keys");
}
else
{
continue;
}
}
ApiFieldMetaData apiFieldMetaData = ApiFieldMetaData.of(field); ApiFieldMetaData apiFieldMetaData = ApiFieldMetaData.of(field);
if(StringUtils.hasContent(apiFieldMetaData.getReplacedByFieldName())) if(StringUtils.hasContent(apiFieldMetaData.getReplacedByFieldName()))
{ {
@ -146,6 +165,11 @@ public class QRecordApiAdapter
} }
else if(associationMap.containsKey(jsonKey)) else if(associationMap.containsKey(jsonKey))
{ {
//////////////////////////////////////////////////////////////////////////////////////////////////
// else, if it's an association - process that (recursively as a list of other records) //
// todo - should probably define in meta-data if an association is included in the api or not!! //
// and what its name is too... //
//////////////////////////////////////////////////////////////////////////////////////////////////
Association association = associationMap.get(jsonKey); Association association = associationMap.get(jsonKey);
Object value = jsonObject.get(jsonKey); Object value = jsonObject.get(jsonKey);
if(value instanceof JSONArray jsonArray) if(value instanceof JSONArray jsonArray)
@ -154,7 +178,7 @@ public class QRecordApiAdapter
{ {
if(subObject instanceof JSONObject subJsonObject) if(subObject instanceof JSONObject subJsonObject)
{ {
QRecord subRecord = apiJsonObjectToQRecord(subJsonObject, association.getAssociatedTableName(), apiVersion); QRecord subRecord = apiJsonObjectToQRecord(subJsonObject, association.getAssociatedTableName(), apiVersion, includePrimaryKey);
qRecord.withAssociatedRecord(association.getName(), subRecord); qRecord.withAssociatedRecord(association.getName(), subRecord);
} }
else else

View File

@ -1066,7 +1066,7 @@ public class QJavalinApiHandler
JSONTokener jsonTokener = new JSONTokener(context.body().trim()); JSONTokener jsonTokener = new JSONTokener(context.body().trim());
JSONObject jsonObject = new JSONObject(jsonTokener); JSONObject jsonObject = new JSONObject(jsonTokener);
insertInput.setRecords(List.of(QRecordApiAdapter.apiJsonObjectToQRecord(jsonObject, tableName, version))); insertInput.setRecords(List.of(QRecordApiAdapter.apiJsonObjectToQRecord(jsonObject, tableName, version, false)));
if(jsonTokener.more()) if(jsonTokener.more())
{ {
@ -1142,7 +1142,7 @@ public class QJavalinApiHandler
for(int i = 0; i < jsonArray.length(); i++) for(int i = 0; i < jsonArray.length(); i++)
{ {
JSONObject jsonObject = jsonArray.getJSONObject(i); JSONObject jsonObject = jsonArray.getJSONObject(i);
recordList.add(QRecordApiAdapter.apiJsonObjectToQRecord(jsonObject, tableName, version)); recordList.add(QRecordApiAdapter.apiJsonObjectToQRecord(jsonObject, tableName, version, false));
} }
if(jsonTokener.more()) if(jsonTokener.more())
@ -1248,7 +1248,7 @@ public class QJavalinApiHandler
for(int i = 0; i < jsonArray.length(); i++) for(int i = 0; i < jsonArray.length(); i++)
{ {
JSONObject jsonObject = jsonArray.getJSONObject(i); JSONObject jsonObject = jsonArray.getJSONObject(i);
recordList.add(QRecordApiAdapter.apiJsonObjectToQRecord(jsonObject, tableName, version)); recordList.add(QRecordApiAdapter.apiJsonObjectToQRecord(jsonObject, tableName, version, true));
} }
if(jsonTokener.more()) if(jsonTokener.more())
@ -1280,23 +1280,47 @@ public class QJavalinApiHandler
// process records to build response // // process records to build response //
/////////////////////////////////////// ///////////////////////////////////////
List<Map<String, Serializable>> response = new ArrayList<>(); List<Map<String, Serializable>> response = new ArrayList<>();
int i = 0;
for(QRecord record : updateOutput.getRecords()) for(QRecord record : updateOutput.getRecords())
{ {
LinkedHashMap<String, Serializable> outputRecord = new LinkedHashMap<>(); LinkedHashMap<String, Serializable> outputRecord = new LinkedHashMap<>();
response.add(outputRecord); response.add(outputRecord);
try
{
QRecord inputRecord = updateInput.getRecords().get(i);
Serializable primaryKey = inputRecord.getValue(table.getPrimaryKeyField());
outputRecord.put(table.getPrimaryKeyField(), primaryKey);
}
catch(Exception e)
{
//////////
// omit //
//////////
}
List<String> errors = record.getErrors(); List<String> errors = record.getErrors();
if(CollectionUtils.nullSafeHasContents(errors)) if(CollectionUtils.nullSafeHasContents(errors))
{
outputRecord.put("error", "Error updating " + table.getLabel() + ": " + StringUtils.joinWithCommasAndAnd(errors));
if(areAnyErrorsNotFound(errors))
{
outputRecord.put("statusCode", HttpStatus.Code.NOT_FOUND.getCode());
outputRecord.put("statusText", HttpStatus.Code.NOT_FOUND.getMessage());
}
else
{ {
outputRecord.put("statusCode", HttpStatus.Code.BAD_REQUEST.getCode()); outputRecord.put("statusCode", HttpStatus.Code.BAD_REQUEST.getCode());
outputRecord.put("statusText", HttpStatus.Code.BAD_REQUEST.getMessage()); outputRecord.put("statusText", HttpStatus.Code.BAD_REQUEST.getMessage());
outputRecord.put("error", "Error updating " + table.getLabel() + ": " + StringUtils.joinWithCommasAndAnd(errors)); }
} }
else else
{ {
outputRecord.put("statusCode", HttpStatus.Code.NO_CONTENT.getCode()); outputRecord.put("statusCode", HttpStatus.Code.NO_CONTENT.getCode());
outputRecord.put("statusText", HttpStatus.Code.NO_CONTENT.getMessage()); outputRecord.put("statusText", HttpStatus.Code.NO_CONTENT.getMessage());
} }
i++;
} }
QJavalinAccessLogger.logEndSuccess(); QJavalinAccessLogger.logEndSuccess();
@ -1312,6 +1336,16 @@ public class QJavalinApiHandler
/*******************************************************************************
**
*******************************************************************************/
private static boolean areAnyErrorsNotFound(List<String> errors)
{
return errors.stream().anyMatch(e -> e.startsWith(UpdateAction.NOT_FOUND_ERROR_PREFIX) || e.startsWith(DeleteAction.NOT_FOUND_ERROR_PREFIX));
}
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
@ -1391,24 +1425,34 @@ public class QJavalinApiHandler
List<Map<String, Serializable>> response = new ArrayList<>(); List<Map<String, Serializable>> response = new ArrayList<>();
List<QRecord> recordsWithErrors = deleteOutput.getRecordsWithErrors(); List<QRecord> recordsWithErrors = deleteOutput.getRecordsWithErrors();
Map<String, String> primaryKeyToErrorMap = new HashMap<>(); Map<String, List<String>> primaryKeyToErrorsMap = new HashMap<>();
for(QRecord recordWithError : CollectionUtils.nonNullList(recordsWithErrors)) for(QRecord recordWithError : CollectionUtils.nonNullList(recordsWithErrors))
{ {
String primaryKey = recordWithError.getValueString(table.getPrimaryKeyField()); String primaryKey = recordWithError.getValueString(table.getPrimaryKeyField());
primaryKeyToErrorMap.put(primaryKey, StringUtils.join(", ", recordWithError.getErrors())); primaryKeyToErrorsMap.put(primaryKey, recordWithError.getErrors());
} }
for(Serializable primaryKey : deleteInput.getPrimaryKeys()) for(Serializable primaryKey : deleteInput.getPrimaryKeys())
{ {
LinkedHashMap<String, Serializable> outputRecord = new LinkedHashMap<>(); LinkedHashMap<String, Serializable> outputRecord = new LinkedHashMap<>();
response.add(outputRecord); response.add(outputRecord);
outputRecord.put(table.getPrimaryKeyField(), primaryKey);
String primaryKeyString = ValueUtils.getValueAsString((primaryKey)); String primaryKeyString = ValueUtils.getValueAsString(primaryKey);
if(primaryKeyToErrorMap.containsKey(primaryKeyString)) List<String> errors = primaryKeyToErrorsMap.get(primaryKeyString);
if(CollectionUtils.nullSafeHasContents(errors))
{
outputRecord.put("error", "Error deleting " + table.getLabel() + ": " + StringUtils.joinWithCommasAndAnd(errors));
if(areAnyErrorsNotFound(errors))
{
outputRecord.put("statusCode", HttpStatus.Code.NOT_FOUND.getCode());
outputRecord.put("statusText", HttpStatus.Code.NOT_FOUND.getMessage());
}
else
{ {
outputRecord.put("statusCode", HttpStatus.Code.BAD_REQUEST.getCode()); outputRecord.put("statusCode", HttpStatus.Code.BAD_REQUEST.getCode());
outputRecord.put("statusText", HttpStatus.Code.BAD_REQUEST.getMessage()); outputRecord.put("statusText", HttpStatus.Code.BAD_REQUEST.getMessage());
outputRecord.put("error", "Error deleting " + table.getLabel() + ": " + primaryKeyToErrorMap.get(primaryKeyString)); }
} }
else else
{ {
@ -1453,20 +1497,6 @@ public class QJavalinApiHandler
PermissionsHelper.checkTablePermissionThrowing(updateInput, TablePermissionSubType.EDIT); PermissionsHelper.checkTablePermissionThrowing(updateInput, TablePermissionSubType.EDIT);
///////////////////////////////////////////////////////
// throw a not found error if the record isn't found //
///////////////////////////////////////////////////////
GetInput getInput = new GetInput();
getInput.setTableName(tableName);
getInput.setPrimaryKey(primaryKey);
GetAction getAction = new GetAction();
GetOutput getOutput = getAction.execute(getInput);
if(getOutput.getRecord() == null)
{
throw (new QNotFoundException("Could not find " + table.getLabel() + " with "
+ table.getFields().get(table.getPrimaryKeyField()).getLabel() + " of " + primaryKey));
}
try try
{ {
if(!StringUtils.hasContent(context.body())) if(!StringUtils.hasContent(context.body()))
@ -1477,7 +1507,7 @@ public class QJavalinApiHandler
JSONTokener jsonTokener = new JSONTokener(context.body().trim()); JSONTokener jsonTokener = new JSONTokener(context.body().trim());
JSONObject jsonObject = new JSONObject(jsonTokener); JSONObject jsonObject = new JSONObject(jsonTokener);
QRecord qRecord = QRecordApiAdapter.apiJsonObjectToQRecord(jsonObject, tableName, version); QRecord qRecord = QRecordApiAdapter.apiJsonObjectToQRecord(jsonObject, tableName, version, false);
qRecord.setValue(table.getPrimaryKeyField(), primaryKey); qRecord.setValue(table.getPrimaryKeyField(), primaryKey);
updateInput.setRecords(List.of(qRecord)); updateInput.setRecords(List.of(qRecord));
@ -1501,8 +1531,18 @@ public class QJavalinApiHandler
List<String> errors = updateOutput.getRecords().get(0).getErrors(); List<String> errors = updateOutput.getRecords().get(0).getErrors();
if(CollectionUtils.nullSafeHasContents(errors)) if(CollectionUtils.nullSafeHasContents(errors))
{ {
if(areAnyErrorsNotFound(errors))
{
throw (new QNotFoundException("Could not find " + table.getLabel() + " with " + table.getFields().get(table.getPrimaryKeyField()).getLabel() + " of " + primaryKey));
}
else
{
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// todo - could be smarter here, about some of these errors being 400, not 500... e.g., a missing required field //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
throw (new QException("Error updating " + table.getLabel() + ": " + StringUtils.joinWithCommasAndAnd(errors))); throw (new QException("Error updating " + table.getLabel() + ": " + StringUtils.joinWithCommasAndAnd(errors)));
} }
}
QJavalinAccessLogger.logEndSuccess(); QJavalinAccessLogger.logEndSuccess();
context.status(HttpStatus.Code.NO_CONTENT.getCode()); context.status(HttpStatus.Code.NO_CONTENT.getCode());
@ -1540,29 +1580,22 @@ public class QJavalinApiHandler
PermissionsHelper.checkTablePermissionThrowing(deleteInput, TablePermissionSubType.DELETE); PermissionsHelper.checkTablePermissionThrowing(deleteInput, TablePermissionSubType.DELETE);
///////////////////////////////////////////////////////
// throw a not found error if the record isn't found //
///////////////////////////////////////////////////////
GetInput getInput = new GetInput();
getInput.setTableName(tableName);
getInput.setPrimaryKey(primaryKey);
GetAction getAction = new GetAction();
GetOutput getOutput = getAction.execute(getInput);
if(getOutput.getRecord() == null)
{
throw (new QNotFoundException("Could not find " + table.getLabel() + " with "
+ table.getFields().get(table.getPrimaryKeyField()).getLabel() + " of " + primaryKey));
}
/////////////////// ///////////////////
// do the delete // // do the delete //
/////////////////// ///////////////////
DeleteAction deleteAction = new DeleteAction(); DeleteAction deleteAction = new DeleteAction();
DeleteOutput deleteOutput = deleteAction.execute(deleteInput); DeleteOutput deleteOutput = deleteAction.execute(deleteInput);
if(CollectionUtils.nullSafeHasContents(deleteOutput.getRecordsWithErrors())) if(CollectionUtils.nullSafeHasContents(deleteOutput.getRecordsWithErrors()))
{
if(areAnyErrorsNotFound(deleteOutput.getRecordsWithErrors().get(0).getErrors()))
{
throw (new QNotFoundException("Could not find " + table.getLabel() + " with " + table.getFields().get(table.getPrimaryKeyField()).getLabel() + " of " + primaryKey));
}
else
{ {
throw (new QException("Error deleting " + table.getLabel() + ": " + StringUtils.joinWithCommasAndAnd(deleteOutput.getRecordsWithErrors().get(0).getErrors()))); throw (new QException("Error deleting " + table.getLabel() + ": " + StringUtils.joinWithCommasAndAnd(deleteOutput.getRecordsWithErrors().get(0).getErrors())));
} }
}
QJavalinAccessLogger.logEndSuccess(); QJavalinAccessLogger.logEndSuccess();
context.status(HttpStatus.Code.NO_CONTENT.getCode()); context.status(HttpStatus.Code.NO_CONTENT.getCode());

View File

@ -97,7 +97,7 @@ class QRecordApiAdapterTest extends BaseTest
/////////////////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////////////////
QRecord recordFromOldApi = QRecordApiAdapter.apiJsonObjectToQRecord(new JSONObject(""" QRecord recordFromOldApi = QRecordApiAdapter.apiJsonObjectToQRecord(new JSONObject("""
{"firstName": "Tim", "shoeCount": 2} {"firstName": "Tim", "shoeCount": 2}
"""), TestUtils.TABLE_NAME_PERSON, TestUtils.V2022_Q4); """), TestUtils.TABLE_NAME_PERSON, TestUtils.V2022_Q4, true);
assertEquals(2, recordFromOldApi.getValueInteger("noOfShoes")); assertEquals(2, recordFromOldApi.getValueInteger("noOfShoes"));
/////////////////////////////////////////// ///////////////////////////////////////////
@ -105,7 +105,7 @@ class QRecordApiAdapterTest extends BaseTest
/////////////////////////////////////////// ///////////////////////////////////////////
QRecord recordFromCurrentApi = QRecordApiAdapter.apiJsonObjectToQRecord(new JSONObject(""" QRecord recordFromCurrentApi = QRecordApiAdapter.apiJsonObjectToQRecord(new JSONObject("""
{"firstName": "Tim", "noOfShoes": 2} {"firstName": "Tim", "noOfShoes": 2}
"""), TestUtils.TABLE_NAME_PERSON, TestUtils.V2023_Q1); """), TestUtils.TABLE_NAME_PERSON, TestUtils.V2023_Q1, true);
assertEquals(2, recordFromCurrentApi.getValueInteger("noOfShoes")); assertEquals(2, recordFromCurrentApi.getValueInteger("noOfShoes"));
///////////////////////////////////////////// /////////////////////////////////////////////
@ -113,7 +113,7 @@ class QRecordApiAdapterTest extends BaseTest
///////////////////////////////////////////// /////////////////////////////////////////////
QRecord recordFromFutureApi = QRecordApiAdapter.apiJsonObjectToQRecord(new JSONObject(""" QRecord recordFromFutureApi = QRecordApiAdapter.apiJsonObjectToQRecord(new JSONObject("""
{"firstName": "Tim", "noOfShoes": 2, "cost": 3.50} {"firstName": "Tim", "noOfShoes": 2, "cost": 3.50}
"""), TestUtils.TABLE_NAME_PERSON, TestUtils.V2023_Q2); """), TestUtils.TABLE_NAME_PERSON, TestUtils.V2023_Q2, true);
assertEquals(2, recordFromFutureApi.getValueInteger("noOfShoes")); assertEquals(2, recordFromFutureApi.getValueInteger("noOfShoes"));
assertEquals(new BigDecimal("3.50"), recordFromFutureApi.getValueBigDecimal("cost")); assertEquals(new BigDecimal("3.50"), recordFromFutureApi.getValueBigDecimal("cost"));
@ -122,7 +122,7 @@ class QRecordApiAdapterTest extends BaseTest
/////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////
QRecord recordWithApiFieldName = QRecordApiAdapter.apiJsonObjectToQRecord(new JSONObject(""" QRecord recordWithApiFieldName = QRecordApiAdapter.apiJsonObjectToQRecord(new JSONObject("""
{"firstName": "Tim", "birthDay": "1976-05-28"} {"firstName": "Tim", "birthDay": "1976-05-28"}
"""), TestUtils.TABLE_NAME_PERSON, TestUtils.V2023_Q2); """), TestUtils.TABLE_NAME_PERSON, TestUtils.V2023_Q2, true);
assertEquals("1976-05-28", recordWithApiFieldName.getValueString("birthDate")); assertEquals("1976-05-28", recordWithApiFieldName.getValueString("birthDate"));
//////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////
@ -130,7 +130,7 @@ class QRecordApiAdapterTest extends BaseTest
//////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////
assertThatThrownBy(() -> QRecordApiAdapter.apiJsonObjectToQRecord(new JSONObject(""" assertThatThrownBy(() -> QRecordApiAdapter.apiJsonObjectToQRecord(new JSONObject("""
{"firstName": "Tim", "noOfShoes": 2} {"firstName": "Tim", "noOfShoes": 2}
"""), TestUtils.TABLE_NAME_PERSON, TestUtils.V2022_Q4)) """), TestUtils.TABLE_NAME_PERSON, TestUtils.V2022_Q4, true))
.isInstanceOf(QBadRequestException.class) .isInstanceOf(QBadRequestException.class)
.hasMessageContaining("unrecognized field name: noOfShoes"); .hasMessageContaining("unrecognized field name: noOfShoes");
@ -139,7 +139,7 @@ class QRecordApiAdapterTest extends BaseTest
///////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////
assertThatThrownBy(() -> QRecordApiAdapter.apiJsonObjectToQRecord(new JSONObject(""" assertThatThrownBy(() -> QRecordApiAdapter.apiJsonObjectToQRecord(new JSONObject("""
{"firstName": "Tim", "cost": 2} {"firstName": "Tim", "cost": 2}
"""), TestUtils.TABLE_NAME_PERSON, TestUtils.V2023_Q1)) """), TestUtils.TABLE_NAME_PERSON, TestUtils.V2023_Q1, true))
.isInstanceOf(QBadRequestException.class) .isInstanceOf(QBadRequestException.class)
.hasMessageContaining("unrecognized field name: cost"); .hasMessageContaining("unrecognized field name: cost");
@ -150,11 +150,29 @@ class QRecordApiAdapterTest extends BaseTest
{ {
assertThatThrownBy(() -> QRecordApiAdapter.apiJsonObjectToQRecord(new JSONObject(""" assertThatThrownBy(() -> QRecordApiAdapter.apiJsonObjectToQRecord(new JSONObject("""
{"firstName": "Tim", "price": 2} {"firstName": "Tim", "price": 2}
"""), TestUtils.TABLE_NAME_PERSON, version)) """), TestUtils.TABLE_NAME_PERSON, version, true))
.isInstanceOf(QBadRequestException.class) .isInstanceOf(QBadRequestException.class)
.hasMessageContaining("unrecognized field name: price"); .hasMessageContaining("unrecognized field name: price");
} }
////////////////////////////////////////////
// assert non-editable fields are omitted //
////////////////////////////////////////////
QRecord recordWithoutNonEditableFields = QRecordApiAdapter.apiJsonObjectToQRecord(new JSONObject("""
{"firstName": "Tim", "birthDay": "1976-05-28", "createDate": "2023-03-31T11:44:28Z", "id": 256}
"""), TestUtils.TABLE_NAME_PERSON, TestUtils.V2023_Q1, false);
assertFalse(recordWithoutNonEditableFields.getValues().containsKey("createDate"));
assertFalse(recordWithoutNonEditableFields.getValues().containsKey("id"));
/////////////////////////////////////////////////////////////////////////
// assert non-editable primary key fields IS included, if so requested //
/////////////////////////////////////////////////////////////////////////
QRecord recordWithoutNonEditablePrimaryKeyFields = QRecordApiAdapter.apiJsonObjectToQRecord(new JSONObject("""
{"firstName": "Tim", "birthDay": "1976-05-28", "createDate": "2023-03-31T11:44:28Z", "id": 256}
"""), TestUtils.TABLE_NAME_PERSON, TestUtils.V2023_Q1, true);
assertFalse(recordWithoutNonEditablePrimaryKeyFields.getValues().containsKey("createDate"));
assertEquals(256, recordWithoutNonEditablePrimaryKeyFields.getValues().get("id"));
} }
} }

View File

@ -796,19 +796,29 @@ class QJavalinApiHandlerTest extends BaseTest
[ [
{"id": 1, "email": "homer@simpson.com"}, {"id": 1, "email": "homer@simpson.com"},
{"id": 2, "email": "marge@simpson.com"}, {"id": 2, "email": "marge@simpson.com"},
{"email": "nobody@simpson.com"} {"email": "nobody@simpson.com"},
{"id": 256, "email": "256@simpson.com"}
] ]
""") """)
.asString(); .asString();
assertEquals(HttpStatus.MULTI_STATUS_207, response.getStatus()); assertEquals(HttpStatus.MULTI_STATUS_207, response.getStatus());
JSONArray jsonArray = new JSONArray(response.getBody()); JSONArray jsonArray = new JSONArray(response.getBody());
assertEquals(3, jsonArray.length()); System.out.println(jsonArray.toString(3));
assertEquals(4, jsonArray.length());
assertEquals(HttpStatus.NO_CONTENT_204, jsonArray.getJSONObject(0).getInt("statusCode")); assertEquals(HttpStatus.NO_CONTENT_204, jsonArray.getJSONObject(0).getInt("statusCode"));
assertEquals(1, jsonArray.getJSONObject(0).getInt("id"));
assertEquals(HttpStatus.NO_CONTENT_204, jsonArray.getJSONObject(1).getInt("statusCode")); assertEquals(HttpStatus.NO_CONTENT_204, jsonArray.getJSONObject(1).getInt("statusCode"));
assertEquals(2, jsonArray.getJSONObject(1).getInt("id"));
assertEquals(HttpStatus.BAD_REQUEST_400, jsonArray.getJSONObject(2).getInt("statusCode")); 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")); assertEquals("Error updating Person: Missing value in primary key field", jsonArray.getJSONObject(2).getString("error"));
assertFalse(jsonArray.getJSONObject(2).has("id"));
assertEquals(HttpStatus.NOT_FOUND_404, jsonArray.getJSONObject(3).getInt("statusCode"));
assertEquals("Error updating Person: No record was found to update for Id = 256", jsonArray.getJSONObject(3).getString("error"));
assertEquals(256, jsonArray.getJSONObject(3).getInt("id"));
QRecord record = getRecord(TestUtils.TABLE_NAME_PERSON, 1); QRecord record = getRecord(TestUtils.TABLE_NAME_PERSON, 1);
assertEquals("homer@simpson.com", record.getValueString("email")); assertEquals("homer@simpson.com", record.getValueString("email"));
@ -818,7 +828,7 @@ class QJavalinApiHandlerTest extends BaseTest
QueryInput queryInput = new QueryInput(); QueryInput queryInput = new QueryInput();
queryInput.setTableName(TestUtils.TABLE_NAME_PERSON); queryInput.setTableName(TestUtils.TABLE_NAME_PERSON);
queryInput.setFilter(new QQueryFilter(new QFilterCriteria("email", QCriteriaOperator.EQUALS, "nobody@simpson.com"))); queryInput.setFilter(new QQueryFilter(new QFilterCriteria("email", QCriteriaOperator.IN, List.of("nobody@simpson.com", "256@simpson.com"))));
QueryOutput queryOutput = new QueryAction().execute(queryInput); QueryOutput queryOutput = new QueryAction().execute(queryInput);
assertEquals(0, queryOutput.getRecords().size()); assertEquals(0, queryOutput.getRecords().size());
} }
@ -885,16 +895,24 @@ class QJavalinApiHandlerTest extends BaseTest
HttpResponse<String> response = Unirest.delete(BASE_URL + "/api/" + VERSION + "/person/bulk") HttpResponse<String> response = Unirest.delete(BASE_URL + "/api/" + VERSION + "/person/bulk")
.body(""" .body("""
[ 1, 3, 5 ] [ 1, 3, 5, 7 ]
""") """)
.asString(); .asString();
assertEquals(HttpStatus.MULTI_STATUS_207, response.getStatus()); assertEquals(HttpStatus.MULTI_STATUS_207, response.getStatus());
JSONArray jsonArray = new JSONArray(response.getBody()); JSONArray jsonArray = new JSONArray(response.getBody());
assertEquals(3, jsonArray.length()); assertEquals(4, jsonArray.length());
assertEquals(HttpStatus.NO_CONTENT_204, jsonArray.getJSONObject(0).getInt("statusCode")); assertEquals(HttpStatus.NO_CONTENT_204, jsonArray.getJSONObject(0).getInt("statusCode"));
assertEquals(1, jsonArray.getJSONObject(0).getInt("id"));
assertEquals(HttpStatus.NO_CONTENT_204, jsonArray.getJSONObject(1).getInt("statusCode")); assertEquals(HttpStatus.NO_CONTENT_204, jsonArray.getJSONObject(1).getInt("statusCode"));
assertEquals(3, jsonArray.getJSONObject(1).getInt("id"));
assertEquals(HttpStatus.NO_CONTENT_204, jsonArray.getJSONObject(2).getInt("statusCode")); assertEquals(HttpStatus.NO_CONTENT_204, jsonArray.getJSONObject(2).getInt("statusCode"));
assertEquals(5, jsonArray.getJSONObject(2).getInt("id"));
assertEquals(HttpStatus.NOT_FOUND_404, jsonArray.getJSONObject(3).getInt("statusCode"));
assertEquals(7, jsonArray.getJSONObject(3).getInt("id"));
QueryInput queryInput = new QueryInput(); QueryInput queryInput = new QueryInput();
queryInput.setTableName(TestUtils.TABLE_NAME_PERSON); queryInput.setTableName(TestUtils.TABLE_NAME_PERSON);