From 084630918fe8235f293b1d8f4f79bbd2df290030 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Fri, 31 Mar 2023 12:11:12 -0500 Subject: [PATCH] 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; --- .../core/actions/tables/DeleteAction.java | 104 ++++- .../core/actions/tables/UpdateAction.java | 4 +- .../actions/tables/delete/DeleteInput.java | 8 +- .../utils/BackendQueryFilterUtils.java | 2 +- .../core/utils/collections/MapBuilder.java | 9 +- .../core/actions/tables/DeleteActionTest.java | 114 +++++- .../core/actions/tables/UpdateActionTest.java | 384 +++++++----------- .../utils/BackendQueryFilterUtilsTest.java | 16 + .../qqq/backend/core/utils/TestUtils.java | 4 + .../utils/collections/MapBuilderTest.java | 16 + .../actions/GenerateOpenApiSpecAction.java | 208 ++++++---- .../qqq/api/actions/QRecordApiAdapter.java | 28 +- .../qqq/api/javalin/QJavalinApiHandler.java | 121 ++++-- .../api/actions/QRecordApiAdapterTest.java | 32 +- .../api/javalin/QJavalinApiHandlerTest.java | 28 +- 15 files changed, 690 insertions(+), 388 deletions(-) diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/DeleteAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/DeleteAction.java index 2e2153d5..415c006b 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/DeleteAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/DeleteAction.java @@ -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 recordListForAudit = getRecordListForAuditIfNeeded(deleteInput); + List recordListForAudit = getRecordListForAuditIfNeeded(deleteInput); + List 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 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 validateRecordsExistAndCanBeAccessed(DeleteInput deleteInput, List oldRecordList) throws QException + { + List recordsWithErrors = new ArrayList<>(); + + QTableMetaData table = deleteInput.getTable(); + QFieldMetaData primaryKeyField = table.getField(table.getPrimaryKeyField()); + + Set primaryKeysToRemoveFromInput = new HashSet<>(); + + List> pages = CollectionUtils.getPages(deleteInput.getPrimaryKeys(), 1000); + for(List page : pages) + { + List primaryKeysToLookup = new ArrayList<>(); + for(Serializable primaryKeyValue : page) + { + if(primaryKeyValue != null) + { + primaryKeysToLookup.add(primaryKeyValue); + } + } + + Map 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 diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/UpdateAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/UpdateAction.java index 29366b21..06d63b06 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/UpdateAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/UpdateAction.java @@ -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); } } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/delete/DeleteInput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/delete/DeleteInput.java index 778118b3..7ffcb203 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/delete/DeleteInput.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/delete/DeleteInput.java @@ -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 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 primaryKeys) { - this.primaryKeys = primaryKeys; + setPrimaryKeys(primaryKeys); return (this); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/utils/BackendQueryFilterUtils.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/utils/BackendQueryFilterUtils.java index bd0a04b3..abf691d8 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/utils/BackendQueryFilterUtils.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/utils/BackendQueryFilterUtils.java @@ -348,7 +348,7 @@ public class BackendQueryFilterUtils } } - if(!criterion.getValues().contains(value)) + if(value == null || !criterion.getValues().contains(value)) { return (false); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/collections/MapBuilder.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/collections/MapBuilder.java index 21b5c667..0a94ddfd 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/collections/MapBuilder.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/collections/MapBuilder.java @@ -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.of(SomeMap::new).with(key, value).with(key2, value2)...build() => SomeMap (the type you specify) *******************************************************************************/ public class MapBuilder { diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/DeleteActionTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/DeleteActionTest.java index eccc6d5c..3c9b2bec 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/DeleteActionTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/DeleteActionTest.java @@ -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 ids) throws QException + { + List 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); + } + } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/UpdateActionTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/UpdateActionTest.java index afea99c3..6b522bc9 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/UpdateActionTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/UpdateActionTest.java @@ -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()); } - */ } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/utils/BackendQueryFilterUtilsTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/utils/BackendQueryFilterUtilsTest.java index 25d9beb0..75c8578c 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/utils/BackendQueryFilterUtilsTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/utils/BackendQueryFilterUtilsTest.java @@ -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")); } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java index a64f65e8..70b74a32 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java @@ -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)) diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/collections/MapBuilderTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/collections/MapBuilderTest.java index 26284df7..b4b17600 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/collections/MapBuilderTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/collections/MapBuilderTest.java @@ -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 map = MapBuilder.of("1", null); + /////////////////////////////////////// // this too, doesn't freaking throw. // /////////////////////////////////////// map.put("2", null); } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testTypeYouRequest() + { + Map myTreeMap = MapBuilder.of(TreeMap::new).with("1", 1).with("2", 2).build(); + assertTrue(myTreeMap instanceof TreeMap); + } + } \ No newline at end of file diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/GenerateOpenApiSpecAction.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/GenerateOpenApiSpecAction.java index f138b7fd..235f6614 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/GenerateOpenApiSpecAction.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/GenerateOpenApiSpecAction.java @@ -92,6 +92,95 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction 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 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, List> fieldListCache = new HashMap<>(); private static Map, Map> 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 diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandler.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandler.java index b26992d3..05c68e45 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandler.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandler.java @@ -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> response = new ArrayList<>(); + int i = 0; for(QRecord record : updateOutput.getRecords()) { LinkedHashMap 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 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 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> response = new ArrayList<>(); - List recordsWithErrors = deleteOutput.getRecordsWithErrors(); - Map primaryKeyToErrorMap = new HashMap<>(); + List recordsWithErrors = deleteOutput.getRecordsWithErrors(); + Map> 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 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 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 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(); diff --git a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/actions/QRecordApiAdapterTest.java b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/actions/QRecordApiAdapterTest.java index ee872107..452d9b5a 100644 --- a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/actions/QRecordApiAdapterTest.java +++ b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/actions/QRecordApiAdapterTest.java @@ -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")); + } } \ No newline at end of file diff --git a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandlerTest.java b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandlerTest.java index e5a7c434..c7f891be 100644 --- a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandlerTest.java +++ b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandlerTest.java @@ -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 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);