mirror of
https://github.com/Kingsrook/qqq.git
synced 2025-07-18 13:10:44 +00:00
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:
@ -24,7 +24,11 @@ package com.kingsrook.qqq.backend.core.actions.tables;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
|
||||
import com.kingsrook.qqq.backend.core.actions.audits.DMLAuditAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.interfaces.DeleteInterface;
|
||||
@ -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.data.QRecord;
|
||||
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.tables.Association;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher;
|
||||
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -57,6 +63,8 @@ public class DeleteAction
|
||||
{
|
||||
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();
|
||||
QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(deleteInput.getBackend());
|
||||
// todo pre-customization - just get to modify the request?
|
||||
|
||||
if(CollectionUtils.nullSafeHasContents(deleteInput.getPrimaryKeys()) && deleteInput.getQueryFilter() != null)
|
||||
{
|
||||
@ -92,13 +99,24 @@ public class DeleteAction
|
||||
}
|
||||
}
|
||||
|
||||
List<QRecord> recordListForAudit = getRecordListForAuditIfNeeded(deleteInput);
|
||||
List<QRecord> recordListForAudit = getRecordListForAuditIfNeeded(deleteInput);
|
||||
List<QRecord> recordsWithValidationErrors = validateRecordsExistAndCanBeAccessed(deleteInput, recordListForAudit);
|
||||
|
||||
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));
|
||||
|
||||
@ -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,
|
||||
** but a scenario where a query filter was passed in - run the query, to
|
||||
|
@ -72,6 +72,8 @@ public class UpdateAction
|
||||
{
|
||||
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))
|
||||
{
|
||||
record.addError("No record was found to update for " + primaryKeyField.getLabel() + " = " + value);
|
||||
record.addError(NOT_FOUND_ERROR_PREFIX + " for " + primaryKeyField.getLabel() + " = " + value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -27,6 +27,7 @@ import java.util.List;
|
||||
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.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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
this.primaryKeys = primaryKeys;
|
||||
setPrimaryKeys(primaryKeys);
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
@ -348,7 +348,7 @@ public class BackendQueryFilterUtils
|
||||
}
|
||||
}
|
||||
|
||||
if(!criterion.getValues().contains(value))
|
||||
if(value == null || !criterion.getValues().contains(value))
|
||||
{
|
||||
return (false);
|
||||
}
|
||||
|
@ -27,10 +27,15 @@ import java.util.Map;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
|
||||
@SuppressWarnings({ "checkstyle:javadoc", "DanglingJavadoc" })
|
||||
/*******************************************************************************
|
||||
** 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
|
||||
** "don't suck"
|
||||
** NPE's on nulls... So, replace it with this, which returns HashMaps (or maps
|
||||
** 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>
|
||||
{
|
||||
|
@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.actions.tables;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import com.kingsrook.qqq.backend.core.BaseTest;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
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.audits.AuditLevel;
|
||||
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.collections.ListBuilder;
|
||||
import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
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
|
||||
@ -66,11 +66,17 @@ class DeleteActionTest extends BaseTest
|
||||
{
|
||||
DeleteInput request = new DeleteInput();
|
||||
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));
|
||||
DeleteOutput result = new DeleteAction().execute(request);
|
||||
assertNotNull(result);
|
||||
assertEquals(2, result.getDeletedRecordCount());
|
||||
assertTrue(CollectionUtils.nullSafeIsEmpty(result.getRecordsWithErrors()));
|
||||
assertEquals(1, result.getDeletedRecordCount());
|
||||
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());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -25,6 +25,7 @@ package com.kingsrook.qqq.backend.core.actions.tables;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import com.kingsrook.qqq.backend.core.BaseTest;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
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.data.QRecord;
|
||||
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.collections.ListBuilder;
|
||||
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));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
/*
|
||||
@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
|
||||
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.setTableName(TestUtils.TABLE_NAME_ORDER);
|
||||
insertInput.setRecords(List.of(
|
||||
new QRecord().withValue("id", 1).withValue("orderNo", "O1").withValue("storeId", 1),
|
||||
new QRecord().withValue("id", 2).withValue("orderNo", "O2").withValue("storeId", 2)
|
||||
));
|
||||
new InsertAction().execute(insertInput);
|
||||
|
||||
insertInput.setTableName(TestUtils.TABLE_NAME_LINE_ITEM);
|
||||
insertInput.setRecords(List.of(
|
||||
new QRecord().withValue("id", 10).withValue("orderId", 1).withValue("sku", "BASIC1"),
|
||||
new QRecord().withValue("id", 20).withValue("orderId", 2).withValue("sku", "BASIC2")
|
||||
));
|
||||
new InsertAction().execute(insertInput);
|
||||
|
||||
insertInput.setTableName(TestUtils.TABLE_NAME_LINE_ITEM_EXTRINSIC);
|
||||
insertInput.setRecords(List.of(
|
||||
new QRecord().withValue("id", 100).withValue("lineItemId", 10).withValue("key", "Key1").withValue("value", "Value1"),
|
||||
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 //
|
||||
///////////////////////////////////////////////////////////
|
||||
{
|
||||
InsertInput insertInput = new InsertInput();
|
||||
insertInput.setTableName(TestUtils.TABLE_NAME_LINE_ITEM);
|
||||
insertInput.setRecords(List.of(new QRecord().withValue("orderId", null).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));
|
||||
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));
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// value in the foreign key to the join-table that provides the security value, but the referenced record isn't found //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////////////
|
||||
// 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. //
|
||||
////////////////////////////////////////////////////////////////////////////////////
|
||||
{
|
||||
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().setSecurityKeyValues(MapBuilder.of(TestUtils.SECURITY_KEY_TYPE_STORE, ListBuilder.of(1)));
|
||||
UpdateInput updateInput = new UpdateInput();
|
||||
updateInput.setTableName(TestUtils.TABLE_NAME_LINE_ITEM);
|
||||
updateInput.setRecords(List.of(new QRecord().withValue("id", 20).withValue("sku", "BASIC3")));
|
||||
UpdateOutput updateOutput = new UpdateAction().execute(updateInput);
|
||||
assertEquals("No record was found to update for Id = 20", updateOutput.getRecords().get(0).getErrors().get(0));
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// 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 //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
{
|
||||
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.setRecords(List.of(
|
||||
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)
|
||||
));
|
||||
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"));
|
||||
QContext.getQSession().setSecurityKeyValues(MapBuilder.of(TestUtils.SECURITY_KEY_TYPE_STORE, ListBuilder.of(1)));
|
||||
UpdateInput updateInput = new UpdateInput();
|
||||
updateInput.setTableName(TestUtils.TABLE_NAME_LINE_ITEM);
|
||||
updateInput.setRecords(List.of(new QRecord().withValue("id", 10).withValue("orderId", 2).withValue("sku", "BASIC3")));
|
||||
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));
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////
|
||||
// try to remove the value that provides the foreign key //
|
||||
///////////////////////////////////////////////////////////
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
// one more time, but with multiple input records referencing each foreign key //
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
InsertInput insertInput = new InsertInput();
|
||||
insertInput.setTableName(TestUtils.TABLE_NAME_LINE_ITEM);
|
||||
insertInput.setRecords(List.of(
|
||||
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),
|
||||
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)
|
||||
));
|
||||
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"));
|
||||
UpdateInput updateInput = new UpdateInput();
|
||||
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"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
/*
|
||||
@Test
|
||||
void testSecurityKeyNullDenied() throws QException
|
||||
{
|
||||
QInstance qInstance = QContext.getQInstance();
|
||||
////////////////////////////////
|
||||
// insert an order in store 1 //
|
||||
////////////////////////////////
|
||||
QContext.getQSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_TYPE_STORE, 1);
|
||||
InsertInput insertInput = new InsertInput();
|
||||
insertInput.setTableName(TestUtils.TABLE_NAME_ORDER);
|
||||
insertInput.setRecords(List.of(new QRecord()));
|
||||
InsertOutput insertOutput = 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());
|
||||
insertInput.setRecords(List.of(new QRecord().withValue("id", 1).withValue("orderNo", "original").withValue("storeId", 1)));
|
||||
new InsertAction().execute(insertInput);
|
||||
|
||||
///////////////////////////////////////////
|
||||
// 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
|
||||
void testSecurityKeyNullAllowed() throws QException
|
||||
{
|
||||
/////////////////////////////////////
|
||||
// change storeId to be allow-null //
|
||||
/////////////////////////////////////
|
||||
QInstance qInstance = QContext.getQInstance();
|
||||
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);
|
||||
InsertInput insertInput = new InsertInput();
|
||||
insertInput.setTableName(TestUtils.TABLE_NAME_ORDER);
|
||||
insertInput.setRecords(List.of(new QRecord()));
|
||||
InsertOutput insertOutput = new InsertAction().execute(insertInput);
|
||||
assertEquals(0, insertOutput.getRecords().get(0).getErrors().size());
|
||||
assertEquals(1, TestUtils.queryTable(TestUtils.TABLE_NAME_ORDER).size());
|
||||
insertInput.setRecords(List.of(new QRecord().withValue("id", 1).withValue("orderNo", "original").withValue("storeId", 1)));
|
||||
new InsertAction().execute(insertInput);
|
||||
|
||||
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
|
||||
void testSecurityKeyAllAccess() throws QException
|
||||
{
|
||||
QInstance qInstance = QContext.getQInstance();
|
||||
qInstance.getTable(TestUtils.TABLE_NAME_ORDER).getRecordSecurityLocks().get(0).setNullValueBehavior(RecordSecurityLock.NullValueBehavior.ALLOW);
|
||||
QContext.getQSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_TYPE_STORE_ALL_ACCESS, true);
|
||||
////////////////////////////////
|
||||
// insert 2 orders in store 1 //
|
||||
////////////////////////////////
|
||||
QContext.getQSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_TYPE_STORE, 1);
|
||||
InsertInput insertInput = new InsertInput();
|
||||
insertInput.setTableName(TestUtils.TABLE_NAME_ORDER);
|
||||
insertInput.setRecords(List.of(
|
||||
new QRecord().withValue("storeId", 999),
|
||||
new QRecord().withValue("storeId", null)
|
||||
new QRecord().withValue("id", 1).withValue("orderNo", "O1").withValue("storeId", 1),
|
||||
new QRecord().withValue("id", 2).withValue("orderNo", "O2").withValue("storeId", 1)
|
||||
));
|
||||
InsertOutput insertOutput = new InsertAction().execute(insertInput);
|
||||
assertEquals(2, TestUtils.queryTable(TestUtils.TABLE_NAME_ORDER).size());
|
||||
new InsertAction().execute(insertInput);
|
||||
|
||||
/////////////////////////////////////////////////////////
|
||||
// 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());
|
||||
}
|
||||
*/
|
||||
|
||||
}
|
||||
|
@ -25,6 +25,7 @@ package com.kingsrook.qqq.backend.core.modules.backend.implementations.utils;
|
||||
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.QFilterCriteria;
|
||||
import com.kingsrook.qqq.backend.core.utils.collections.ListBuilder;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
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, "T%"), "f", "Rest"));
|
||||
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"));
|
||||
}
|
||||
|
||||
|
||||
|
@ -619,6 +619,10 @@ public class TestUtils
|
||||
.withName(TABLE_NAME_ORDER_EXTRINSIC)
|
||||
.withBackendName(MEMORY_BACKEND_NAME)
|
||||
.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("createDate", QFieldType.DATE_TIME).withIsEditable(false))
|
||||
.withField(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME).withIsEditable(false))
|
||||
|
@ -23,8 +23,10 @@ package com.kingsrook.qqq.backend.core.utils.collections;
|
||||
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
import org.junit.jupiter.api.Test;
|
||||
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);
|
||||
|
||||
///////////////////////////////////////
|
||||
// this too, doesn't freaking throw. //
|
||||
///////////////////////////////////////
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
@ -92,6 +92,95 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
|
||||
{
|
||||
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=<value`
|
||||
* Greater Than: `myField=>value`
|
||||
* Less Than or Equals: `myField=<=value`
|
||||
* Greater Than or Equals: `myField=>=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<>();
|
||||
// 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()
|
||||
.withFlows(MapBuilder.of("clientCredentials", new OAuth2Flow()
|
||||
.withTokenUrl("/api/oauth/token"))));
|
||||
@ -297,35 +387,20 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
|
||||
.withAllOf(ListBuilder.of(
|
||||
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 //
|
||||
//////////////////////////////////////
|
||||
Method queryGet = new Method()
|
||||
.withSummary("Search for " + tableLabel + " records by query string")
|
||||
.withDescription("""
|
||||
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=<value`
|
||||
* Greater Than: `myField=>value`
|
||||
* Less Than or Equals: `myField=<=value`
|
||||
* Greater Than or Equals: `myField=>=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)
|
||||
""")
|
||||
.withDescription(QUERY_DESCRIPTION)
|
||||
.withOperationId("query" + tableApiNameUcFirst)
|
||||
.withTags(ListBuilder.of(tableLabel))
|
||||
.withParameters(ListBuilder.of(
|
||||
@ -390,9 +465,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
|
||||
|
||||
Method idGet = new Method()
|
||||
.withSummary("Get one " + tableLabel + " by " + primaryKeyLabel)
|
||||
.withDescription("""
|
||||
Get one record from this table, by specifying its primary key as a path parameter.
|
||||
""")
|
||||
.withDescription(GET_DESCRIPTION)
|
||||
.withOperationId("get" + tableApiNameUcFirst)
|
||||
.withTags(ListBuilder.of(tableLabel))
|
||||
.withParameters(ListBuilder.of(
|
||||
@ -412,16 +485,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
|
||||
|
||||
Method idPatch = new Method()
|
||||
.withSummary("Update one " + tableLabel)
|
||||
.withDescription("""
|
||||
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.
|
||||
""")
|
||||
.withDescription(UPDATE_DESCRIPTION)
|
||||
.withOperationId("update" + tableApiNameUcFirst)
|
||||
.withTags(ListBuilder.of(tableLabel))
|
||||
.withParameters(ListBuilder.of(
|
||||
@ -443,11 +507,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
|
||||
|
||||
Method idDelete = new Method()
|
||||
.withSummary("Delete one " + tableLabel)
|
||||
.withDescription("""
|
||||
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.
|
||||
""")
|
||||
.withDescription(DELETE_DESCRIPTION)
|
||||
.withOperationId("delete" + tableApiNameUcFirst)
|
||||
.withTags(ListBuilder.of(tableLabel))
|
||||
.withParameters(ListBuilder.of(
|
||||
@ -473,12 +533,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
|
||||
|
||||
Method slashPost = new Method()
|
||||
.withSummary("Create one " + tableLabel)
|
||||
.withDescription("""
|
||||
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.
|
||||
""")
|
||||
.withDescription(INSERT_DESCRIPTION)
|
||||
.withRequestBody(new RequestBody()
|
||||
.withRequired(true)
|
||||
.withDescription("Values for the " + tableLabel + " record to create.")
|
||||
@ -508,14 +563,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
|
||||
////////////////
|
||||
Method bulkPost = new Method()
|
||||
.withSummary("Create multiple " + tableLabel + " records")
|
||||
.withDescription("""
|
||||
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.
|
||||
""")
|
||||
.withDescription(BULK_INSERT_DESCRIPTION)
|
||||
.withRequestBody(new RequestBody()
|
||||
.withRequired(true)
|
||||
.withDescription("Values for the " + tableLabel + " records to create.")
|
||||
@ -530,15 +578,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
|
||||
|
||||
Method bulkPatch = new Method()
|
||||
.withSummary("Update multiple " + tableLabel + " records")
|
||||
.withDescription("""
|
||||
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.
|
||||
""")
|
||||
.withDescription(BULK_UPDATE_DESCRIPTION)
|
||||
.withRequestBody(new RequestBody()
|
||||
.withRequired(true)
|
||||
.withDescription("Values for the " + tableLabel + " records to update.")
|
||||
@ -559,12 +599,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
|
||||
|
||||
Method bulkDelete = new Method()
|
||||
.withSummary("Delete multiple " + tableLabel + " records")
|
||||
.withDescription("""
|
||||
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.
|
||||
""")
|
||||
.withDescription(BULK_DELETE_DESCRIPTION)
|
||||
.withRequestBody(new RequestBody()
|
||||
.withRequired(true)
|
||||
.withDescription(primaryKeyLabel + " values for the " + tableLabel + " records to delete.")
|
||||
@ -835,20 +870,34 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
|
||||
case "patch" -> ListBuilder.of(
|
||||
MapBuilder.of(LinkedHashMap::new)
|
||||
.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)
|
||||
.with("statusCode", HttpStatus.BAD_REQUEST.getCode())
|
||||
.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(
|
||||
MapBuilder.of(LinkedHashMap::new)
|
||||
.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)
|
||||
.with("statusCode", HttpStatus.BAD_REQUEST.getCode())
|
||||
.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));
|
||||
};
|
||||
@ -857,10 +906,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
|
||||
properties.put("statusCode", new Schema().withType("integer"));
|
||||
properties.put("statusText", 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()
|
||||
.withDescription("Multiple statuses. See body for details.")
|
||||
@ -870,9 +916,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
|
||||
.withItems(new Schema()
|
||||
.withType("object")
|
||||
.withProperties(properties))
|
||||
.withExample(example)
|
||||
)
|
||||
));
|
||||
.withExample(example))));
|
||||
}
|
||||
|
||||
|
||||
|
@ -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.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.Association;
|
||||
@ -50,6 +51,8 @@ import org.json.JSONObject;
|
||||
*******************************************************************************/
|
||||
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>, 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 //
|
||||
@ -134,6 +137,22 @@ public class QRecordApiAdapter
|
||||
QFieldMetaData field = apiFieldsMap.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);
|
||||
if(StringUtils.hasContent(apiFieldMetaData.getReplacedByFieldName()))
|
||||
{
|
||||
@ -146,6 +165,11 @@ public class QRecordApiAdapter
|
||||
}
|
||||
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);
|
||||
Object value = jsonObject.get(jsonKey);
|
||||
if(value instanceof JSONArray jsonArray)
|
||||
@ -154,7 +178,7 @@ public class QRecordApiAdapter
|
||||
{
|
||||
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);
|
||||
}
|
||||
else
|
||||
|
@ -1066,7 +1066,7 @@ public class QJavalinApiHandler
|
||||
JSONTokener jsonTokener = new JSONTokener(context.body().trim());
|
||||
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())
|
||||
{
|
||||
@ -1142,7 +1142,7 @@ public class QJavalinApiHandler
|
||||
for(int i = 0; i < jsonArray.length(); i++)
|
||||
{
|
||||
JSONObject jsonObject = jsonArray.getJSONObject(i);
|
||||
recordList.add(QRecordApiAdapter.apiJsonObjectToQRecord(jsonObject, tableName, version));
|
||||
recordList.add(QRecordApiAdapter.apiJsonObjectToQRecord(jsonObject, tableName, version, false));
|
||||
}
|
||||
|
||||
if(jsonTokener.more())
|
||||
@ -1248,7 +1248,7 @@ public class QJavalinApiHandler
|
||||
for(int i = 0; i < jsonArray.length(); i++)
|
||||
{
|
||||
JSONObject jsonObject = jsonArray.getJSONObject(i);
|
||||
recordList.add(QRecordApiAdapter.apiJsonObjectToQRecord(jsonObject, tableName, version));
|
||||
recordList.add(QRecordApiAdapter.apiJsonObjectToQRecord(jsonObject, tableName, version, true));
|
||||
}
|
||||
|
||||
if(jsonTokener.more())
|
||||
@ -1280,23 +1280,47 @@ public class QJavalinApiHandler
|
||||
// process records to build response //
|
||||
///////////////////////////////////////
|
||||
List<Map<String, Serializable>> response = new ArrayList<>();
|
||||
int i = 0;
|
||||
for(QRecord record : updateOutput.getRecords())
|
||||
{
|
||||
LinkedHashMap<String, Serializable> outputRecord = new LinkedHashMap<>();
|
||||
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();
|
||||
if(CollectionUtils.nullSafeHasContents(errors))
|
||||
{
|
||||
outputRecord.put("statusCode", HttpStatus.Code.BAD_REQUEST.getCode());
|
||||
outputRecord.put("statusText", HttpStatus.Code.BAD_REQUEST.getMessage());
|
||||
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("statusText", HttpStatus.Code.BAD_REQUEST.getMessage());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
outputRecord.put("statusCode", HttpStatus.Code.NO_CONTENT.getCode());
|
||||
outputRecord.put("statusText", HttpStatus.Code.NO_CONTENT.getMessage());
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -1390,25 +1424,35 @@ public class QJavalinApiHandler
|
||||
///////////////////////////////////////
|
||||
List<Map<String, Serializable>> response = new ArrayList<>();
|
||||
|
||||
List<QRecord> recordsWithErrors = deleteOutput.getRecordsWithErrors();
|
||||
Map<String, String> primaryKeyToErrorMap = new HashMap<>();
|
||||
List<QRecord> recordsWithErrors = deleteOutput.getRecordsWithErrors();
|
||||
Map<String, List<String>> primaryKeyToErrorsMap = new HashMap<>();
|
||||
for(QRecord recordWithError : CollectionUtils.nonNullList(recordsWithErrors))
|
||||
{
|
||||
String primaryKey = recordWithError.getValueString(table.getPrimaryKeyField());
|
||||
primaryKeyToErrorMap.put(primaryKey, StringUtils.join(", ", recordWithError.getErrors()));
|
||||
primaryKeyToErrorsMap.put(primaryKey, recordWithError.getErrors());
|
||||
}
|
||||
|
||||
for(Serializable primaryKey : deleteInput.getPrimaryKeys())
|
||||
{
|
||||
LinkedHashMap<String, Serializable> outputRecord = new LinkedHashMap<>();
|
||||
response.add(outputRecord);
|
||||
outputRecord.put(table.getPrimaryKeyField(), primaryKey);
|
||||
|
||||
String primaryKeyString = ValueUtils.getValueAsString((primaryKey));
|
||||
if(primaryKeyToErrorMap.containsKey(primaryKeyString))
|
||||
String primaryKeyString = ValueUtils.getValueAsString(primaryKey);
|
||||
List<String> errors = primaryKeyToErrorsMap.get(primaryKeyString);
|
||||
if(CollectionUtils.nullSafeHasContents(errors))
|
||||
{
|
||||
outputRecord.put("statusCode", HttpStatus.Code.BAD_REQUEST.getCode());
|
||||
outputRecord.put("statusText", HttpStatus.Code.BAD_REQUEST.getMessage());
|
||||
outputRecord.put("error", "Error deleting " + table.getLabel() + ": " + primaryKeyToErrorMap.get(primaryKeyString));
|
||||
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("statusText", HttpStatus.Code.BAD_REQUEST.getMessage());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -1453,20 +1497,6 @@ public class QJavalinApiHandler
|
||||
|
||||
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
|
||||
{
|
||||
if(!StringUtils.hasContent(context.body()))
|
||||
@ -1477,7 +1507,7 @@ public class QJavalinApiHandler
|
||||
JSONTokener jsonTokener = new JSONTokener(context.body().trim());
|
||||
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);
|
||||
updateInput.setRecords(List.of(qRecord));
|
||||
|
||||
@ -1501,7 +1531,17 @@ public class QJavalinApiHandler
|
||||
List<String> errors = updateOutput.getRecords().get(0).getErrors();
|
||||
if(CollectionUtils.nullSafeHasContents(errors))
|
||||
{
|
||||
throw (new QException("Error updating " + table.getLabel() + ": " + StringUtils.joinWithCommasAndAnd(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)));
|
||||
}
|
||||
}
|
||||
|
||||
QJavalinAccessLogger.logEndSuccess();
|
||||
@ -1540,20 +1580,6 @@ public class QJavalinApiHandler
|
||||
|
||||
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 //
|
||||
///////////////////
|
||||
@ -1561,7 +1587,14 @@ public class QJavalinApiHandler
|
||||
DeleteOutput deleteOutput = deleteAction.execute(deleteInput);
|
||||
if(CollectionUtils.nullSafeHasContents(deleteOutput.getRecordsWithErrors()))
|
||||
{
|
||||
throw (new QException("Error deleting " + table.getLabel() + ": " + StringUtils.joinWithCommasAndAnd(deleteOutput.getRecordsWithErrors().get(0).getErrors())));
|
||||
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())));
|
||||
}
|
||||
}
|
||||
|
||||
QJavalinAccessLogger.logEndSuccess();
|
||||
|
@ -97,7 +97,7 @@ class QRecordApiAdapterTest extends BaseTest
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
QRecord recordFromOldApi = QRecordApiAdapter.apiJsonObjectToQRecord(new JSONObject("""
|
||||
{"firstName": "Tim", "shoeCount": 2}
|
||||
"""), TestUtils.TABLE_NAME_PERSON, TestUtils.V2022_Q4);
|
||||
"""), TestUtils.TABLE_NAME_PERSON, TestUtils.V2022_Q4, true);
|
||||
assertEquals(2, recordFromOldApi.getValueInteger("noOfShoes"));
|
||||
|
||||
///////////////////////////////////////////
|
||||
@ -105,7 +105,7 @@ class QRecordApiAdapterTest extends BaseTest
|
||||
///////////////////////////////////////////
|
||||
QRecord recordFromCurrentApi = QRecordApiAdapter.apiJsonObjectToQRecord(new JSONObject("""
|
||||
{"firstName": "Tim", "noOfShoes": 2}
|
||||
"""), TestUtils.TABLE_NAME_PERSON, TestUtils.V2023_Q1);
|
||||
"""), TestUtils.TABLE_NAME_PERSON, TestUtils.V2023_Q1, true);
|
||||
assertEquals(2, recordFromCurrentApi.getValueInteger("noOfShoes"));
|
||||
|
||||
/////////////////////////////////////////////
|
||||
@ -113,7 +113,7 @@ class QRecordApiAdapterTest extends BaseTest
|
||||
/////////////////////////////////////////////
|
||||
QRecord recordFromFutureApi = QRecordApiAdapter.apiJsonObjectToQRecord(new JSONObject("""
|
||||
{"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(new BigDecimal("3.50"), recordFromFutureApi.getValueBigDecimal("cost"));
|
||||
|
||||
@ -122,7 +122,7 @@ class QRecordApiAdapterTest extends BaseTest
|
||||
///////////////////////////////////////////////////////////////////
|
||||
QRecord recordWithApiFieldName = QRecordApiAdapter.apiJsonObjectToQRecord(new JSONObject("""
|
||||
{"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"));
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -130,7 +130,7 @@ class QRecordApiAdapterTest extends BaseTest
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
assertThatThrownBy(() -> QRecordApiAdapter.apiJsonObjectToQRecord(new JSONObject("""
|
||||
{"firstName": "Tim", "noOfShoes": 2}
|
||||
"""), TestUtils.TABLE_NAME_PERSON, TestUtils.V2022_Q4))
|
||||
"""), TestUtils.TABLE_NAME_PERSON, TestUtils.V2022_Q4, true))
|
||||
.isInstanceOf(QBadRequestException.class)
|
||||
.hasMessageContaining("unrecognized field name: noOfShoes");
|
||||
|
||||
@ -139,7 +139,7 @@ class QRecordApiAdapterTest extends BaseTest
|
||||
/////////////////////////////////////////////////////////////////////////
|
||||
assertThatThrownBy(() -> QRecordApiAdapter.apiJsonObjectToQRecord(new JSONObject("""
|
||||
{"firstName": "Tim", "cost": 2}
|
||||
"""), TestUtils.TABLE_NAME_PERSON, TestUtils.V2023_Q1))
|
||||
"""), TestUtils.TABLE_NAME_PERSON, TestUtils.V2023_Q1, true))
|
||||
.isInstanceOf(QBadRequestException.class)
|
||||
.hasMessageContaining("unrecognized field name: cost");
|
||||
|
||||
@ -150,11 +150,29 @@ class QRecordApiAdapterTest extends BaseTest
|
||||
{
|
||||
assertThatThrownBy(() -> QRecordApiAdapter.apiJsonObjectToQRecord(new JSONObject("""
|
||||
{"firstName": "Tim", "price": 2}
|
||||
"""), TestUtils.TABLE_NAME_PERSON, version))
|
||||
"""), TestUtils.TABLE_NAME_PERSON, version, true))
|
||||
.isInstanceOf(QBadRequestException.class)
|
||||
.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"));
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -796,19 +796,29 @@ class QJavalinApiHandlerTest extends BaseTest
|
||||
[
|
||||
{"id": 1, "email": "homer@simpson.com"},
|
||||
{"id": 2, "email": "marge@simpson.com"},
|
||||
{"email": "nobody@simpson.com"}
|
||||
{"email": "nobody@simpson.com"},
|
||||
{"id": 256, "email": "256@simpson.com"}
|
||||
]
|
||||
""")
|
||||
.asString();
|
||||
assertEquals(HttpStatus.MULTI_STATUS_207, response.getStatus());
|
||||
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(1, jsonArray.getJSONObject(0).getInt("id"));
|
||||
|
||||
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("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);
|
||||
assertEquals("homer@simpson.com", record.getValueString("email"));
|
||||
@ -818,7 +828,7 @@ class QJavalinApiHandlerTest extends BaseTest
|
||||
|
||||
QueryInput queryInput = new QueryInput();
|
||||
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);
|
||||
assertEquals(0, queryOutput.getRecords().size());
|
||||
}
|
||||
@ -885,16 +895,24 @@ class QJavalinApiHandlerTest extends BaseTest
|
||||
|
||||
HttpResponse<String> response = Unirest.delete(BASE_URL + "/api/" + VERSION + "/person/bulk")
|
||||
.body("""
|
||||
[ 1, 3, 5 ]
|
||||
[ 1, 3, 5, 7 ]
|
||||
""")
|
||||
.asString();
|
||||
assertEquals(HttpStatus.MULTI_STATUS_207, response.getStatus());
|
||||
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(1, jsonArray.getJSONObject(0).getInt("id"));
|
||||
|
||||
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(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.setTableName(TestUtils.TABLE_NAME_PERSON);
|
||||
|
Reference in New Issue
Block a user