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.io.Serializable;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
|
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
|
||||||
import com.kingsrook.qqq.backend.core.actions.audits.DMLAuditAction;
|
import com.kingsrook.qqq.backend.core.actions.audits.DMLAuditAction;
|
||||||
import com.kingsrook.qqq.backend.core.actions.interfaces.DeleteInterface;
|
import com.kingsrook.qqq.backend.core.actions.interfaces.DeleteInterface;
|
||||||
@ -41,12 +45,14 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
|
|||||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
|
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
|
||||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.audits.AuditLevel;
|
import com.kingsrook.qqq.backend.core.model.metadata.audits.AuditLevel;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
|
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.Association;
|
import com.kingsrook.qqq.backend.core.model.metadata.tables.Association;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||||
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher;
|
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher;
|
||||||
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
|
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
|
||||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||||
|
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
@ -57,6 +63,8 @@ public class DeleteAction
|
|||||||
{
|
{
|
||||||
private static final QLogger LOG = QLogger.getLogger(DeleteAction.class);
|
private static final QLogger LOG = QLogger.getLogger(DeleteAction.class);
|
||||||
|
|
||||||
|
public static final String NOT_FOUND_ERROR_PREFIX = "No record was found to delete";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
@ -68,7 +76,6 @@ public class DeleteAction
|
|||||||
|
|
||||||
QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher();
|
QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher();
|
||||||
QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(deleteInput.getBackend());
|
QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(deleteInput.getBackend());
|
||||||
// todo pre-customization - just get to modify the request?
|
|
||||||
|
|
||||||
if(CollectionUtils.nullSafeHasContents(deleteInput.getPrimaryKeys()) && deleteInput.getQueryFilter() != null)
|
if(CollectionUtils.nullSafeHasContents(deleteInput.getPrimaryKeys()) && deleteInput.getQueryFilter() != null)
|
||||||
{
|
{
|
||||||
@ -93,12 +100,23 @@ public class DeleteAction
|
|||||||
}
|
}
|
||||||
|
|
||||||
List<QRecord> recordListForAudit = getRecordListForAuditIfNeeded(deleteInput);
|
List<QRecord> recordListForAudit = getRecordListForAuditIfNeeded(deleteInput);
|
||||||
|
List<QRecord> recordsWithValidationErrors = validateRecordsExistAndCanBeAccessed(deleteInput, recordListForAudit);
|
||||||
|
|
||||||
DeleteOutput deleteOutput = deleteInterface.execute(deleteInput);
|
DeleteOutput deleteOutput = deleteInterface.execute(deleteInput);
|
||||||
|
|
||||||
manageAssociations(deleteInput);
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// merge the backend's output with any validation errors we found (whose ids wouldn't have gotten into the backend delete) //
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
List<QRecord> outputRecordsWithErrors = deleteOutput.getRecordsWithErrors();
|
||||||
|
if(outputRecordsWithErrors == null)
|
||||||
|
{
|
||||||
|
deleteOutput.setRecordsWithErrors(new ArrayList<>());
|
||||||
|
outputRecordsWithErrors = deleteOutput.getRecordsWithErrors();
|
||||||
|
}
|
||||||
|
|
||||||
// todo post-customization - can do whatever w/ the result if you want
|
outputRecordsWithErrors.addAll(recordsWithValidationErrors);
|
||||||
|
|
||||||
|
manageAssociations(deleteInput);
|
||||||
|
|
||||||
new DMLAuditAction().execute(new DMLAuditInput().withTableActionInput(deleteInput).withRecordList(recordListForAudit));
|
new DMLAuditAction().execute(new DMLAuditInput().withTableActionInput(deleteInput).withRecordList(recordListForAudit));
|
||||||
|
|
||||||
@ -188,6 +206,84 @@ public class DeleteAction
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Note - the "can be accessed" part of this method name - it implies that
|
||||||
|
** records that you can't see because of security - that they won't be found
|
||||||
|
** by the query here, so it's the same to you as if they don't exist at all!
|
||||||
|
**
|
||||||
|
** This method, if it finds any missing records, will:
|
||||||
|
** - remove those ids from the deleteInput
|
||||||
|
** - create a QRecord with that id and a not-found error message.
|
||||||
|
*******************************************************************************/
|
||||||
|
private List<QRecord> validateRecordsExistAndCanBeAccessed(DeleteInput deleteInput, List<QRecord> oldRecordList) throws QException
|
||||||
|
{
|
||||||
|
List<QRecord> recordsWithErrors = new ArrayList<>();
|
||||||
|
|
||||||
|
QTableMetaData table = deleteInput.getTable();
|
||||||
|
QFieldMetaData primaryKeyField = table.getField(table.getPrimaryKeyField());
|
||||||
|
|
||||||
|
Set<Serializable> primaryKeysToRemoveFromInput = new HashSet<>();
|
||||||
|
|
||||||
|
List<List<Serializable>> pages = CollectionUtils.getPages(deleteInput.getPrimaryKeys(), 1000);
|
||||||
|
for(List<Serializable> page : pages)
|
||||||
|
{
|
||||||
|
List<Serializable> primaryKeysToLookup = new ArrayList<>();
|
||||||
|
for(Serializable primaryKeyValue : page)
|
||||||
|
{
|
||||||
|
if(primaryKeyValue != null)
|
||||||
|
{
|
||||||
|
primaryKeysToLookup.add(primaryKeyValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<Serializable, QRecord> lookedUpRecords = new HashMap<>();
|
||||||
|
if(CollectionUtils.nullSafeHasContents(oldRecordList))
|
||||||
|
{
|
||||||
|
for(QRecord record : oldRecordList)
|
||||||
|
{
|
||||||
|
lookedUpRecords.put(record.getValue(table.getPrimaryKeyField()), record);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if(!primaryKeysToLookup.isEmpty())
|
||||||
|
{
|
||||||
|
QueryInput queryInput = new QueryInput();
|
||||||
|
queryInput.setTableName(table.getName());
|
||||||
|
queryInput.setFilter(new QQueryFilter(new QFilterCriteria(table.getPrimaryKeyField(), QCriteriaOperator.IN, primaryKeysToLookup)));
|
||||||
|
QueryOutput queryOutput = new QueryAction().execute(queryInput);
|
||||||
|
for(QRecord record : queryOutput.getRecords())
|
||||||
|
{
|
||||||
|
lookedUpRecords.put(record.getValue(table.getPrimaryKeyField()), record);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for(Serializable primaryKeyValue : page)
|
||||||
|
{
|
||||||
|
primaryKeyValue = ValueUtils.getValueAsFieldType(primaryKeyField.getType(), primaryKeyValue);
|
||||||
|
if(!lookedUpRecords.containsKey(primaryKeyValue))
|
||||||
|
{
|
||||||
|
QRecord recordWithError = new QRecord();
|
||||||
|
recordsWithErrors.add(recordWithError);
|
||||||
|
recordWithError.setValue(primaryKeyField.getName(), primaryKeyValue);
|
||||||
|
recordWithError.addError(NOT_FOUND_ERROR_PREFIX + " for " + primaryKeyField.getLabel() + " = " + primaryKeyValue);
|
||||||
|
primaryKeysToRemoveFromInput.add(primaryKeyValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////
|
||||||
|
// do one mass removal of any bad keys from the input key list //
|
||||||
|
/////////////////////////////////////////////////////////////////
|
||||||
|
if(!primaryKeysToRemoveFromInput.isEmpty())
|
||||||
|
{
|
||||||
|
deleteInput.getPrimaryKeys().removeAll(primaryKeysToRemoveFromInput);
|
||||||
|
primaryKeysToRemoveFromInput.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (recordsWithErrors);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** For an implementation that doesn't support a queryFilter as its input,
|
** For an implementation that doesn't support a queryFilter as its input,
|
||||||
** but a scenario where a query filter was passed in - run the query, to
|
** but a scenario where a query filter was passed in - run the query, to
|
||||||
|
@ -72,6 +72,8 @@ public class UpdateAction
|
|||||||
{
|
{
|
||||||
private static final QLogger LOG = QLogger.getLogger(UpdateAction.class);
|
private static final QLogger LOG = QLogger.getLogger(UpdateAction.class);
|
||||||
|
|
||||||
|
public static final String NOT_FOUND_ERROR_PREFIX = "No record was found to update";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
@ -193,7 +195,7 @@ public class UpdateAction
|
|||||||
|
|
||||||
if(!lookedUpRecords.containsKey(value))
|
if(!lookedUpRecords.containsKey(value))
|
||||||
{
|
{
|
||||||
record.addError("No record was found to update for " + primaryKeyField.getLabel() + " = " + value);
|
record.addError(NOT_FOUND_ERROR_PREFIX + " for " + primaryKeyField.getLabel() + " = " + value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,6 +27,7 @@ import java.util.List;
|
|||||||
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
|
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
|
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||||
|
import com.kingsrook.qqq.backend.core.utils.collections.MutableList;
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
@ -101,7 +102,10 @@ public class DeleteInput extends AbstractTableActionInput
|
|||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public void setPrimaryKeys(List<Serializable> primaryKeys)
|
public void setPrimaryKeys(List<Serializable> primaryKeys)
|
||||||
{
|
{
|
||||||
this.primaryKeys = primaryKeys;
|
///////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// the action may edit this list (e.g., to remove keys w/ errors), so wrap it in MutableList //
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
this.primaryKeys = new MutableList<>(primaryKeys);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -112,7 +116,7 @@ public class DeleteInput extends AbstractTableActionInput
|
|||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public DeleteInput withPrimaryKeys(List<Serializable> primaryKeys)
|
public DeleteInput withPrimaryKeys(List<Serializable> primaryKeys)
|
||||||
{
|
{
|
||||||
this.primaryKeys = primaryKeys;
|
setPrimaryKeys(primaryKeys);
|
||||||
return (this);
|
return (this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -348,7 +348,7 @@ public class BackendQueryFilterUtils
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!criterion.getValues().contains(value))
|
if(value == null || !criterion.getValues().contains(value))
|
||||||
{
|
{
|
||||||
return (false);
|
return (false);
|
||||||
}
|
}
|
||||||
|
@ -27,10 +27,15 @@ import java.util.Map;
|
|||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
|
||||||
|
@SuppressWarnings({ "checkstyle:javadoc", "DanglingJavadoc" })
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Map.of is "great", but annoying because it makes unmodifiable maps, and it
|
** Map.of is "great", but annoying because it makes unmodifiable maps, and it
|
||||||
** NPE's on nulls... So, replace it with this, which returns HashMaps, which
|
** NPE's on nulls... So, replace it with this, which returns HashMaps (or maps
|
||||||
** "don't suck"
|
** of the type you choose).
|
||||||
|
**
|
||||||
|
** Can use it 2 ways:
|
||||||
|
** MapBuilder.of(key, value, key2, value2, ...) => Map (a HashMap)
|
||||||
|
** MapBuilder.<KeyType ValueType>of(SomeMap::new).with(key, value).with(key2, value2)...build() => SomeMap (the type you specify)
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public class MapBuilder<K, V>
|
public class MapBuilder<K, V>
|
||||||
{
|
{
|
||||||
|
@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.actions.tables;
|
|||||||
|
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
import com.kingsrook.qqq.backend.core.BaseTest;
|
import com.kingsrook.qqq.backend.core.BaseTest;
|
||||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||||
@ -40,8 +41,9 @@ import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
|||||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.audits.AuditLevel;
|
import com.kingsrook.qqq.backend.core.model.metadata.audits.AuditLevel;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.audits.QAuditRules;
|
import com.kingsrook.qqq.backend.core.model.metadata.audits.QAuditRules;
|
||||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
|
||||||
import com.kingsrook.qqq.backend.core.utils.TestUtils;
|
import com.kingsrook.qqq.backend.core.utils.TestUtils;
|
||||||
|
import com.kingsrook.qqq.backend.core.utils.collections.ListBuilder;
|
||||||
|
import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
@ -57,8 +59,6 @@ class DeleteActionTest extends BaseTest
|
|||||||
{
|
{
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** At the core level, there isn't much that can be asserted, as it uses the
|
|
||||||
** mock implementation - just confirming that all of the "wiring" works.
|
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
@Test
|
@Test
|
||||||
@ -66,11 +66,17 @@ class DeleteActionTest extends BaseTest
|
|||||||
{
|
{
|
||||||
DeleteInput request = new DeleteInput();
|
DeleteInput request = new DeleteInput();
|
||||||
request.setTableName("person");
|
request.setTableName("person");
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// the mock backend - it'll find a record for id=1, but not for id=2 - so we can test both a found & deleted, and a not-found here //
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
request.setPrimaryKeys(List.of(1, 2));
|
request.setPrimaryKeys(List.of(1, 2));
|
||||||
DeleteOutput result = new DeleteAction().execute(request);
|
DeleteOutput result = new DeleteAction().execute(request);
|
||||||
assertNotNull(result);
|
assertNotNull(result);
|
||||||
assertEquals(2, result.getDeletedRecordCount());
|
assertEquals(1, result.getDeletedRecordCount());
|
||||||
assertTrue(CollectionUtils.nullSafeIsEmpty(result.getRecordsWithErrors()));
|
assertEquals(1, result.getRecordsWithErrors().size());
|
||||||
|
assertEquals(2, result.getRecordsWithErrors().get(0).getValueInteger("id"));
|
||||||
|
assertEquals("No record was found to delete for Id = 2", result.getRecordsWithErrors().get(0).getErrors().get(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -295,4 +301,102 @@ class DeleteActionTest extends BaseTest
|
|||||||
return (queryOutput.getRecords());
|
return (queryOutput.getRecords());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
void testSecurityKeys() throws QException
|
||||||
|
{
|
||||||
|
QContext.getQSession().setSecurityKeyValues(MapBuilder.of(TestUtils.SECURITY_KEY_TYPE_STORE, ListBuilder.of(1)));
|
||||||
|
insert2OrdersWith3Lines3LineExtrinsicsAnd4OrderExtrinsicAssociations();
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////
|
||||||
|
// make sure we inserted the records we think we did //
|
||||||
|
///////////////////////////////////////////////////////
|
||||||
|
assertIdsExist(TestUtils.TABLE_NAME_ORDER, List.of(1, 2));
|
||||||
|
assertIdsExist(TestUtils.TABLE_NAME_LINE_ITEM, List.of(1, 2, 3));
|
||||||
|
assertIdsExist(TestUtils.TABLE_NAME_LINE_ITEM_EXTRINSIC, List.of(1, 2, 3));
|
||||||
|
assertIdsExist(TestUtils.TABLE_NAME_ORDER_EXTRINSIC, List.of(1, 2, 3, 4));
|
||||||
|
|
||||||
|
QContext.getQSession().setSecurityKeyValues(MapBuilder.of(TestUtils.SECURITY_KEY_TYPE_STORE, ListBuilder.of(2)));
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////
|
||||||
|
// assert can't delete the records at any level //
|
||||||
|
//////////////////////////////////////////////////
|
||||||
|
DeleteInput deleteInput = new DeleteInput();
|
||||||
|
deleteInput.setTableName(TestUtils.TABLE_NAME_ORDER);
|
||||||
|
deleteInput.setPrimaryKeys(List.of(1));
|
||||||
|
DeleteOutput deleteOutput = new DeleteAction().execute(deleteInput);
|
||||||
|
assertEquals(0, deleteOutput.getDeletedRecordCount());
|
||||||
|
assertEquals(1, deleteOutput.getRecordsWithErrors().size());
|
||||||
|
assertEquals("No record was found to delete for Id = 1", deleteOutput.getRecordsWithErrors().get(0).getErrors().get(0));
|
||||||
|
|
||||||
|
deleteInput.setTableName(TestUtils.TABLE_NAME_LINE_ITEM);
|
||||||
|
deleteInput.setPrimaryKeys(List.of(1));
|
||||||
|
deleteOutput = new DeleteAction().execute(deleteInput);
|
||||||
|
assertEquals(0, deleteOutput.getDeletedRecordCount());
|
||||||
|
assertEquals(1, deleteOutput.getRecordsWithErrors().size());
|
||||||
|
assertEquals("No record was found to delete for Id = 1", deleteOutput.getRecordsWithErrors().get(0).getErrors().get(0));
|
||||||
|
|
||||||
|
deleteInput.setTableName(TestUtils.TABLE_NAME_LINE_ITEM_EXTRINSIC);
|
||||||
|
deleteInput.setPrimaryKeys(List.of(1));
|
||||||
|
deleteOutput = new DeleteAction().execute(deleteInput);
|
||||||
|
assertEquals(0, deleteOutput.getDeletedRecordCount());
|
||||||
|
assertEquals(1, deleteOutput.getRecordsWithErrors().size());
|
||||||
|
assertEquals("No record was found to delete for Id = 1", deleteOutput.getRecordsWithErrors().get(0).getErrors().get(0));
|
||||||
|
|
||||||
|
deleteInput.setTableName(TestUtils.TABLE_NAME_ORDER_EXTRINSIC);
|
||||||
|
deleteInput.setPrimaryKeys(List.of(1));
|
||||||
|
deleteOutput = new DeleteAction().execute(deleteInput);
|
||||||
|
assertEquals(0, deleteOutput.getDeletedRecordCount());
|
||||||
|
assertEquals(1, deleteOutput.getRecordsWithErrors().size());
|
||||||
|
assertEquals("No record was found to delete for Id = 1", deleteOutput.getRecordsWithErrors().get(0).getErrors().get(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
private void assertIdsExist(String tableName, List<Integer> ids) throws QException
|
||||||
|
{
|
||||||
|
List<QRecord> records = TestUtils.queryTable(tableName);
|
||||||
|
for(Integer id : ids)
|
||||||
|
{
|
||||||
|
assertTrue(records.stream().anyMatch(r -> Objects.equals(id, r.getValueInteger("id"))));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
private static void insert2OrdersWith3Lines3LineExtrinsicsAnd4OrderExtrinsicAssociations() throws QException
|
||||||
|
{
|
||||||
|
InsertInput insertInput = new InsertInput();
|
||||||
|
insertInput.setTableName(TestUtils.TABLE_NAME_ORDER);
|
||||||
|
insertInput.setRecords(List.of(
|
||||||
|
new QRecord().withValue("storeId", 1).withValue("orderNo", "ORD123")
|
||||||
|
|
||||||
|
.withAssociatedRecord("orderLine", new QRecord().withValue("sku", "BASIC1").withValue("quantity", 1)
|
||||||
|
.withAssociatedRecord("extrinsics", new QRecord().withValue("key", "LINE-EXT-1.1").withValue("value", "LINE-VAL-1")))
|
||||||
|
|
||||||
|
.withAssociatedRecord("orderLine", new QRecord().withValue("sku", "BASIC2").withValue("quantity", 2)
|
||||||
|
.withAssociatedRecord("extrinsics", new QRecord().withValue("key", "LINE-EXT-2.1").withValue("value", "LINE-VAL-2"))
|
||||||
|
.withAssociatedRecord("extrinsics", new QRecord().withValue("key", "LINE-EXT-2.2").withValue("value", "LINE-VAL-3")))
|
||||||
|
|
||||||
|
.withAssociatedRecord("extrinsics", new QRecord().withValue("key", "MY-FIELD-1").withValue("value", "MY-VALUE-1"))
|
||||||
|
.withAssociatedRecord("extrinsics", new QRecord().withValue("key", "MY-FIELD-2").withValue("value", "MY-VALUE-2"))
|
||||||
|
.withAssociatedRecord("extrinsics", new QRecord().withValue("key", "MY-FIELD-3").withValue("value", "MY-VALUE-3")),
|
||||||
|
|
||||||
|
new QRecord().withValue("storeId", 1).withValue("orderNo", "ORD124")
|
||||||
|
.withAssociatedRecord("orderLine", new QRecord().withValue("sku", "BASIC3").withValue("quantity", 3))
|
||||||
|
.withAssociatedRecord("extrinsics", new QRecord().withValue("key", "YOUR-FIELD-1").withValue("value", "YOUR-VALUE-1"))
|
||||||
|
));
|
||||||
|
new InsertAction().execute(insertInput);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,7 @@ package com.kingsrook.qqq.backend.core.actions.tables;
|
|||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
import com.kingsrook.qqq.backend.core.BaseTest;
|
import com.kingsrook.qqq.backend.core.BaseTest;
|
||||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||||
@ -33,6 +34,7 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
|
|||||||
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateOutput;
|
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateOutput;
|
||||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
|
||||||
import com.kingsrook.qqq.backend.core.utils.TestUtils;
|
import com.kingsrook.qqq.backend.core.utils.TestUtils;
|
||||||
import com.kingsrook.qqq.backend.core.utils.collections.ListBuilder;
|
import com.kingsrook.qqq.backend.core.utils.collections.ListBuilder;
|
||||||
import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder;
|
import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder;
|
||||||
@ -393,237 +395,114 @@ class UpdateActionTest extends BaseTest
|
|||||||
assertEquals("Missing value in required field: Order No", updateOutput.getRecords().get(3).getErrors().get(0));
|
assertEquals("Missing value in required field: Order No", updateOutput.getRecords().get(3).getErrors().get(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
/*******************************************************************************
|
|
||||||
**
|
|
||||||
*******************************************************************************/
|
|
||||||
/*
|
|
||||||
@Test
|
|
||||||
void testInsertMultiLevelSecurityJoins() throws QException
|
|
||||||
{
|
|
||||||
QContext.getQSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_TYPE_STORE, 1);
|
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
// null value in the foreign key to the join-table that provides the security value //
|
|
||||||
//////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
{
|
|
||||||
InsertInput insertInput = new InsertInput();
|
|
||||||
insertInput.setTableName(TestUtils.TABLE_NAME_LINE_ITEM_EXTRINSIC);
|
|
||||||
insertInput.setRecords(List.of(new QRecord().withValue("lineItemId", null).withValue("key", "kidsCanCallYou").withValue("value", "HoJu")));
|
|
||||||
InsertOutput insertOutput = new InsertAction().execute(insertInput);
|
|
||||||
assertEquals("You do not have permission to insert this record - the referenced Order was not found.", insertOutput.getRecords().get(0).getErrors().get(0));
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
// value in the foreign key to the join-table that provides the security value, but the referenced record isn't found //
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
{
|
|
||||||
InsertInput insertInput = new InsertInput();
|
|
||||||
insertInput.setTableName(TestUtils.TABLE_NAME_LINE_ITEM_EXTRINSIC);
|
|
||||||
insertInput.setRecords(List.of(new QRecord().withValue("lineItemId", 1701).withValue("key", "kidsCanCallYou").withValue("value", "HoJu")));
|
|
||||||
InsertOutput insertOutput = new InsertAction().execute(insertInput);
|
|
||||||
assertEquals("You do not have permission to insert this record - the referenced Order was not found.", insertOutput.getRecords().get(0).getErrors().get(0));
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
// insert an order and lineItem with storeId=2 - then, reset our session to only have storeId=1 in it - and try to insert an order-line referencing that order. //
|
|
||||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
QContext.getQSession().withSecurityKeyValues(new HashMap<>());
|
|
||||||
QContext.getQSession().withSecurityKeyValues(TestUtils.SECURITY_KEY_TYPE_STORE, List.of(2));
|
|
||||||
InsertInput insertOrderInput = new InsertInput();
|
|
||||||
insertOrderInput.setTableName(TestUtils.TABLE_NAME_ORDER);
|
|
||||||
insertOrderInput.setRecords(List.of(new QRecord().withValue("id", 42).withValue("storeId", 2)));
|
|
||||||
InsertOutput insertOrderOutput = new InsertAction().execute(insertOrderInput);
|
|
||||||
assertEquals(42, insertOrderOutput.getRecords().get(0).getValueInteger("id"));
|
|
||||||
|
|
||||||
InsertInput insertLineItemInput = new InsertInput();
|
|
||||||
insertLineItemInput.setTableName(TestUtils.TABLE_NAME_LINE_ITEM);
|
|
||||||
insertLineItemInput.setRecords(List.of(new QRecord().withValue("id", 4200).withValue("orderId", 42).withValue("sku", "BASIC1").withValue("quantity", 24)));
|
|
||||||
InsertOutput insertLineItemOutput = new InsertAction().execute(insertLineItemInput);
|
|
||||||
assertEquals(4200, insertLineItemOutput.getRecords().get(0).getValueInteger("id"));
|
|
||||||
|
|
||||||
QContext.getQSession().withSecurityKeyValues(new HashMap<>());
|
|
||||||
QContext.getQSession().withSecurityKeyValues(TestUtils.SECURITY_KEY_TYPE_STORE, List.of(1));
|
|
||||||
InsertInput insertLineItemExtrinsicInput = new InsertInput();
|
|
||||||
insertLineItemExtrinsicInput.setTableName(TestUtils.TABLE_NAME_LINE_ITEM_EXTRINSIC);
|
|
||||||
insertLineItemExtrinsicInput.setRecords(List.of(new QRecord().withValue("lineItemId", 4200).withValue("key", "kidsCanCallYou").withValue("value", "HoJu")));
|
|
||||||
InsertOutput insertLineItemExtrinsicOutput = new InsertAction().execute(insertLineItemExtrinsicInput);
|
|
||||||
assertEquals("You do not have permission to insert this record.", insertLineItemExtrinsicOutput.getRecords().get(0).getErrors().get(0));
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
QContext.getQSession().withSecurityKeyValues(new HashMap<>());
|
|
||||||
QContext.getQSession().withSecurityKeyValues(TestUtils.SECURITY_KEY_TYPE_STORE, List.of(1));
|
|
||||||
InsertInput insertOrderInput = new InsertInput();
|
|
||||||
insertOrderInput.setTableName(TestUtils.TABLE_NAME_ORDER);
|
|
||||||
insertOrderInput.setRecords(List.of(new QRecord().withValue("id", 47).withValue("storeId", 1)));
|
|
||||||
InsertOutput insertOrderOutput = new InsertAction().execute(insertOrderInput);
|
|
||||||
assertEquals(47, insertOrderOutput.getRecords().get(0).getValueInteger("id"));
|
|
||||||
|
|
||||||
InsertInput insertLineItemInput = new InsertInput();
|
|
||||||
insertLineItemInput.setTableName(TestUtils.TABLE_NAME_LINE_ITEM);
|
|
||||||
insertLineItemInput.setRecords(List.of(new QRecord().withValue("id", 4700).withValue("orderId", 47).withValue("sku", "BASIC1").withValue("quantity", 74)));
|
|
||||||
InsertOutput insertLineItemOutput = new InsertAction().execute(insertLineItemInput);
|
|
||||||
assertEquals(4700, insertLineItemOutput.getRecords().get(0).getValueInteger("id"));
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////
|
|
||||||
// combine all the above, plus one record that works //
|
|
||||||
///////////////////////////////////////////////////////
|
|
||||||
InsertInput insertInput = new InsertInput();
|
|
||||||
insertInput.setTableName(TestUtils.TABLE_NAME_LINE_ITEM_EXTRINSIC);
|
|
||||||
insertInput.setRecords(List.of(
|
|
||||||
new QRecord().withValue("lineItemId", null).withValue("key", "theKidsCanCallYou").withValue("value", "HoJu"),
|
|
||||||
new QRecord().withValue("lineItemId", 1701).withValue("key", "theKidsCanCallYou").withValue("value", "HoJu"),
|
|
||||||
new QRecord().withValue("lineItemId", 4200).withValue("key", "theKidsCanCallYou").withValue("value", "HoJu"),
|
|
||||||
new QRecord().withValue("lineItemId", 4700).withValue("key", "theKidsCanCallYou").withValue("value", "HoJu")
|
|
||||||
));
|
|
||||||
InsertOutput insertOutput = new InsertAction().execute(insertInput);
|
|
||||||
assertEquals("You do not have permission to insert this record - the referenced Order was not found.", insertOutput.getRecords().get(0).getErrors().get(0));
|
|
||||||
assertEquals("You do not have permission to insert this record - the referenced Order was not found.", insertOutput.getRecords().get(1).getErrors().get(0));
|
|
||||||
assertEquals("You do not have permission to insert this record.", insertOutput.getRecords().get(2).getErrors().get(0));
|
|
||||||
assertEquals(0, insertOutput.getRecords().get(3).getErrors().size());
|
|
||||||
assertNotNull(insertOutput.getRecords().get(3).getValueInteger("id"));
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
/////////////////////////////////////////////////////////////////////////////////
|
|
||||||
// one more time, but with multiple input records referencing each foreign key //
|
|
||||||
/////////////////////////////////////////////////////////////////////////////////
|
|
||||||
InsertInput insertInput = new InsertInput();
|
|
||||||
insertInput.setTableName(TestUtils.TABLE_NAME_LINE_ITEM_EXTRINSIC);
|
|
||||||
insertInput.setRecords(List.of(
|
|
||||||
new QRecord().withValue("lineItemId", null).withValue("key", "theKidsCanCallYou").withValue("value", "HoJu"),
|
|
||||||
new QRecord().withValue("lineItemId", 1701).withValue("key", "theKidsCanCallYou").withValue("value", "HoJu"),
|
|
||||||
new QRecord().withValue("lineItemId", 4200).withValue("key", "theKidsCanCallYou").withValue("value", "HoJu"),
|
|
||||||
new QRecord().withValue("lineItemId", 4700).withValue("key", "theKidsCanCallYou").withValue("value", "HoJu"),
|
|
||||||
new QRecord().withValue("lineItemId", null).withValue("key", "theKidsCanCallYou").withValue("value", "HoJu"),
|
|
||||||
new QRecord().withValue("lineItemId", 1701).withValue("key", "theKidsCanCallYou").withValue("value", "HoJu"),
|
|
||||||
new QRecord().withValue("lineItemId", 4200).withValue("key", "theKidsCanCallYou").withValue("value", "HoJu"),
|
|
||||||
new QRecord().withValue("lineItemId", 4700).withValue("key", "theKidsCanCallYou").withValue("value", "HoJu")
|
|
||||||
));
|
|
||||||
InsertOutput insertOutput = new InsertAction().execute(insertInput);
|
|
||||||
assertEquals("You do not have permission to insert this record - the referenced Order was not found.", insertOutput.getRecords().get(0).getErrors().get(0));
|
|
||||||
assertEquals("You do not have permission to insert this record - the referenced Order was not found.", insertOutput.getRecords().get(1).getErrors().get(0));
|
|
||||||
assertEquals("You do not have permission to insert this record.", insertOutput.getRecords().get(2).getErrors().get(0));
|
|
||||||
assertEquals(0, insertOutput.getRecords().get(3).getErrors().size());
|
|
||||||
assertNotNull(insertOutput.getRecords().get(3).getValueInteger("id"));
|
|
||||||
assertEquals("You do not have permission to insert this record - the referenced Order was not found.", insertOutput.getRecords().get(4).getErrors().get(0));
|
|
||||||
assertEquals("You do not have permission to insert this record - the referenced Order was not found.", insertOutput.getRecords().get(5).getErrors().get(0));
|
|
||||||
assertEquals("You do not have permission to insert this record.", insertOutput.getRecords().get(6).getErrors().get(0));
|
|
||||||
assertEquals(0, insertOutput.getRecords().get(7).getErrors().size());
|
|
||||||
assertNotNull(insertOutput.getRecords().get(7).getValueInteger("id"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
/*
|
|
||||||
@Test
|
@Test
|
||||||
void testInsertSingleLevelSecurityJoins() throws QException
|
void testUpdateSecurityJoins() throws QException
|
||||||
{
|
{
|
||||||
QContext.getQSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_TYPE_STORE, 1);
|
QContext.getQSession().setSecurityKeyValues(MapBuilder.of(TestUtils.SECURITY_KEY_TYPE_STORE, ListBuilder.of(1, 2)));
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////
|
||||||
// null value in the foreign key to the join-table that provides the security value //
|
// insert an order in each of store 1 and store 2 //
|
||||||
//////////////////////////////////////////////////////////////////////////////////////
|
// with some lines and line-extrinsics //
|
||||||
{
|
////////////////////////////////////////////////////
|
||||||
InsertInput insertInput = new InsertInput();
|
InsertInput insertInput = new InsertInput();
|
||||||
insertInput.setTableName(TestUtils.TABLE_NAME_LINE_ITEM);
|
insertInput.setTableName(TestUtils.TABLE_NAME_ORDER);
|
||||||
insertInput.setRecords(List.of(new QRecord().withValue("orderId", null).withValue("sku", "BASIC1").withValue("quantity", 1)));
|
insertInput.setRecords(List.of(
|
||||||
InsertOutput insertOutput = new InsertAction().execute(insertInput);
|
new QRecord().withValue("id", 1).withValue("orderNo", "O1").withValue("storeId", 1),
|
||||||
assertEquals("You do not have permission to insert this record - the referenced Order was not found.", insertOutput.getRecords().get(0).getErrors().get(0));
|
new QRecord().withValue("id", 2).withValue("orderNo", "O2").withValue("storeId", 2)
|
||||||
}
|
));
|
||||||
|
new InsertAction().execute(insertInput);
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
// value in the foreign key to the join-table that provides the security value, but the referenced record isn't found //
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
{
|
|
||||||
InsertInput insertInput = new InsertInput();
|
|
||||||
insertInput.setTableName(TestUtils.TABLE_NAME_LINE_ITEM);
|
|
||||||
insertInput.setRecords(List.of(new QRecord().withValue("orderId", 1701).withValue("sku", "BASIC1").withValue("quantity", 1)));
|
|
||||||
InsertOutput insertOutput = new InsertAction().execute(insertInput);
|
|
||||||
assertEquals("You do not have permission to insert this record - the referenced Order was not found.", insertOutput.getRecords().get(0).getErrors().get(0));
|
|
||||||
}
|
|
||||||
{
|
|
||||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
// insert an order with storeId=2 - then, reset our session to only have storeId=1 in it - and try to insert an order-line referencing that order. //
|
|
||||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
QContext.getQSession().withSecurityKeyValues(new HashMap<>());
|
|
||||||
QContext.getQSession().withSecurityKeyValues(TestUtils.SECURITY_KEY_TYPE_STORE, List.of(2));
|
|
||||||
InsertInput insertOrderInput = new InsertInput();
|
|
||||||
insertOrderInput.setTableName(TestUtils.TABLE_NAME_ORDER);
|
|
||||||
insertOrderInput.setRecords(List.of(new QRecord().withValue("id", 42).withValue("storeId", 2)));
|
|
||||||
InsertOutput insertOrderOutput = new InsertAction().execute(insertOrderInput);
|
|
||||||
assertEquals(42, insertOrderOutput.getRecords().get(0).getValueInteger("id"));
|
|
||||||
|
|
||||||
QContext.getQSession().withSecurityKeyValues(new HashMap<>());
|
|
||||||
QContext.getQSession().withSecurityKeyValues(TestUtils.SECURITY_KEY_TYPE_STORE, List.of(1));
|
|
||||||
InsertInput insertLineItemInput = new InsertInput();
|
|
||||||
insertLineItemInput.setTableName(TestUtils.TABLE_NAME_LINE_ITEM);
|
|
||||||
insertLineItemInput.setRecords(List.of(new QRecord().withValue("orderId", 42).withValue("sku", "BASIC1").withValue("quantity", 1)));
|
|
||||||
InsertOutput insertLineItemOutput = new InsertAction().execute(insertLineItemInput);
|
|
||||||
assertEquals("You do not have permission to insert this record.", insertLineItemOutput.getRecords().get(0).getErrors().get(0));
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
QContext.getQSession().withSecurityKeyValues(new HashMap<>());
|
|
||||||
QContext.getQSession().withSecurityKeyValues(TestUtils.SECURITY_KEY_TYPE_STORE, List.of(1));
|
|
||||||
InsertInput insertOrderInput = new InsertInput();
|
|
||||||
insertOrderInput.setTableName(TestUtils.TABLE_NAME_ORDER);
|
|
||||||
insertOrderInput.setRecords(List.of(new QRecord().withValue("id", 47).withValue("storeId", 1)));
|
|
||||||
new InsertAction().execute(insertOrderInput);
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////
|
|
||||||
// combine all the above, plus one record that works //
|
|
||||||
///////////////////////////////////////////////////////
|
|
||||||
InsertInput insertInput = new InsertInput();
|
|
||||||
insertInput.setTableName(TestUtils.TABLE_NAME_LINE_ITEM);
|
insertInput.setTableName(TestUtils.TABLE_NAME_LINE_ITEM);
|
||||||
insertInput.setRecords(List.of(
|
insertInput.setRecords(List.of(
|
||||||
new QRecord().withValue("orderId", null).withValue("sku", "BASIC1").withValue("quantity", 1),
|
new QRecord().withValue("id", 10).withValue("orderId", 1).withValue("sku", "BASIC1"),
|
||||||
new QRecord().withValue("orderId", 1701).withValue("sku", "BASIC1").withValue("quantity", 1),
|
new QRecord().withValue("id", 20).withValue("orderId", 2).withValue("sku", "BASIC2")
|
||||||
new QRecord().withValue("orderId", 42).withValue("sku", "BASIC1").withValue("quantity", 1),
|
|
||||||
new QRecord().withValue("orderId", 47).withValue("sku", "BASIC1").withValue("quantity", 1)
|
|
||||||
));
|
));
|
||||||
InsertOutput insertOutput = new InsertAction().execute(insertInput);
|
new InsertAction().execute(insertInput);
|
||||||
assertEquals("You do not have permission to insert this record - the referenced Order was not found.", insertOutput.getRecords().get(0).getErrors().get(0));
|
|
||||||
assertEquals("You do not have permission to insert this record - the referenced Order was not found.", insertOutput.getRecords().get(1).getErrors().get(0));
|
insertInput.setTableName(TestUtils.TABLE_NAME_LINE_ITEM_EXTRINSIC);
|
||||||
assertEquals("You do not have permission to insert this record.", insertOutput.getRecords().get(2).getErrors().get(0));
|
insertInput.setRecords(List.of(
|
||||||
assertEquals(0, insertOutput.getRecords().get(3).getErrors().size());
|
new QRecord().withValue("id", 100).withValue("lineItemId", 10).withValue("key", "Key1").withValue("value", "Value1"),
|
||||||
assertNotNull(insertOutput.getRecords().get(3).getValueInteger("id"));
|
new QRecord().withValue("id", 200).withValue("lineItemId", 20).withValue("key", "Key2").withValue("value", "Value2")
|
||||||
|
));
|
||||||
|
new InsertAction().execute(insertInput);
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////
|
||||||
|
// try to remove the value that provides the foreign key //
|
||||||
|
///////////////////////////////////////////////////////////
|
||||||
|
{
|
||||||
|
UpdateInput updateInput = new UpdateInput();
|
||||||
|
updateInput.setTableName(TestUtils.TABLE_NAME_LINE_ITEM);
|
||||||
|
updateInput.setRecords(List.of(new QRecord().withValue("id", 10).withValue("orderId", null).withValue("sku", "BASIC2")));
|
||||||
|
UpdateOutput updateOutput = new UpdateAction().execute(updateInput);
|
||||||
|
assertEquals("You do not have permission to update this record - the referenced Order was not found.", updateOutput.getRecords().get(0).getErrors().get(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// with a session that can only access store 1, try to update the line in store 2 //
|
||||||
|
// should fail as a not-found - you can't see that record. //
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////
|
||||||
{
|
{
|
||||||
/////////////////////////////////////////////////////////////////////////////////
|
QContext.getQSession().setSecurityKeyValues(MapBuilder.of(TestUtils.SECURITY_KEY_TYPE_STORE, ListBuilder.of(1)));
|
||||||
// one more time, but with multiple input records referencing each foreign key //
|
UpdateInput updateInput = new UpdateInput();
|
||||||
/////////////////////////////////////////////////////////////////////////////////
|
updateInput.setTableName(TestUtils.TABLE_NAME_LINE_ITEM);
|
||||||
InsertInput insertInput = new InsertInput();
|
updateInput.setRecords(List.of(new QRecord().withValue("id", 20).withValue("sku", "BASIC3")));
|
||||||
insertInput.setTableName(TestUtils.TABLE_NAME_LINE_ITEM);
|
UpdateOutput updateOutput = new UpdateAction().execute(updateInput);
|
||||||
insertInput.setRecords(List.of(
|
assertEquals("No record was found to update for Id = 20", updateOutput.getRecords().get(0).getErrors().get(0));
|
||||||
new QRecord().withValue("orderId", null).withValue("sku", "BASIC1").withValue("quantity", 1),
|
}
|
||||||
new QRecord().withValue("orderId", 1701).withValue("sku", "BASIC1").withValue("quantity", 1),
|
|
||||||
new QRecord().withValue("orderId", 42).withValue("sku", "BASIC1").withValue("quantity", 1),
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
new QRecord().withValue("orderId", 47).withValue("sku", "BASIC1").withValue("quantity", 1),
|
// with a session that can only access store 1, try to update the line from the order in store 1 to be in store 2 //
|
||||||
new QRecord().withValue("orderId", null).withValue("sku", "BASIC1").withValue("quantity", 1),
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
new QRecord().withValue("orderId", 1701).withValue("sku", "BASIC1").withValue("quantity", 1),
|
{
|
||||||
new QRecord().withValue("orderId", 42).withValue("sku", "BASIC1").withValue("quantity", 1),
|
QContext.getQSession().setSecurityKeyValues(MapBuilder.of(TestUtils.SECURITY_KEY_TYPE_STORE, ListBuilder.of(1)));
|
||||||
new QRecord().withValue("orderId", 47).withValue("sku", "BASIC1").withValue("quantity", 1)
|
UpdateInput updateInput = new UpdateInput();
|
||||||
));
|
updateInput.setTableName(TestUtils.TABLE_NAME_LINE_ITEM);
|
||||||
InsertOutput insertOutput = new InsertAction().execute(insertInput);
|
updateInput.setRecords(List.of(new QRecord().withValue("id", 10).withValue("orderId", 2).withValue("sku", "BASIC3")));
|
||||||
assertEquals("You do not have permission to insert this record - the referenced Order was not found.", insertOutput.getRecords().get(0).getErrors().get(0));
|
UpdateOutput updateOutput = new UpdateAction().execute(updateInput);
|
||||||
assertEquals("You do not have permission to insert this record - the referenced Order was not found.", insertOutput.getRecords().get(1).getErrors().get(0));
|
assertEquals("You do not have permission to update this record - the referenced Order was not found.", updateOutput.getRecords().get(0).getErrors().get(0));
|
||||||
assertEquals("You do not have permission to insert this record.", insertOutput.getRecords().get(2).getErrors().get(0));
|
}
|
||||||
assertEquals(0, insertOutput.getRecords().get(3).getErrors().size());
|
|
||||||
assertNotNull(insertOutput.getRecords().get(3).getValueInteger("id"));
|
///////////////////////////////////////////////////////////
|
||||||
assertEquals("You do not have permission to insert this record - the referenced Order was not found.", insertOutput.getRecords().get(4).getErrors().get(0));
|
// try to remove the value that provides the foreign key //
|
||||||
assertEquals("You do not have permission to insert this record - the referenced Order was not found.", insertOutput.getRecords().get(5).getErrors().get(0));
|
///////////////////////////////////////////////////////////
|
||||||
assertEquals("You do not have permission to insert this record.", insertOutput.getRecords().get(6).getErrors().get(0));
|
{
|
||||||
assertEquals(0, insertOutput.getRecords().get(7).getErrors().size());
|
UpdateInput updateInput = new UpdateInput();
|
||||||
assertNotNull(insertOutput.getRecords().get(7).getValueInteger("id"));
|
updateInput.setTableName(TestUtils.TABLE_NAME_LINE_ITEM_EXTRINSIC);
|
||||||
|
updateInput.setRecords(List.of(new QRecord().withValue("id", 100).withValue("lineItemId", null).withValue("key", "updatedKey")));
|
||||||
|
UpdateOutput updateOutput = new UpdateAction().execute(updateInput);
|
||||||
|
assertEquals("You do not have permission to update this record - the referenced Order was not found.", updateOutput.getRecords().get(0).getErrors().get(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// with a session that can only access store 1, try to update the line-extrinsic in store 2 //
|
||||||
|
// should fail as a not-found - you can't see that record. //
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
{
|
||||||
|
QContext.getQSession().setSecurityKeyValues(MapBuilder.of(TestUtils.SECURITY_KEY_TYPE_STORE, ListBuilder.of(1)));
|
||||||
|
UpdateInput updateInput = new UpdateInput();
|
||||||
|
updateInput.setTableName(TestUtils.TABLE_NAME_LINE_ITEM_EXTRINSIC);
|
||||||
|
updateInput.setRecords(List.of(new QRecord().withValue("id", 200).withValue("key", "updatedKey")));
|
||||||
|
UpdateOutput updateOutput = new UpdateAction().execute(updateInput);
|
||||||
|
assertEquals("No record was found to update for Id = 200", updateOutput.getRecords().get(0).getErrors().get(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// with a session that can only access store 1, try to update the line-extrinsic from the order in store 1 to be in store 2 //
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
{
|
||||||
|
QContext.getQSession().setSecurityKeyValues(MapBuilder.of(TestUtils.SECURITY_KEY_TYPE_STORE, ListBuilder.of(1)));
|
||||||
|
UpdateInput updateInput = new UpdateInput();
|
||||||
|
updateInput.setTableName(TestUtils.TABLE_NAME_LINE_ITEM_EXTRINSIC);
|
||||||
|
updateInput.setRecords(List.of(new QRecord().withValue("id", 100).withValue("lineItemId", 20).withValue("key", "updatedKey")));
|
||||||
|
UpdateOutput updateOutput = new UpdateAction().execute(updateInput);
|
||||||
|
assertEquals("You do not have permission to update this record - the referenced Order was not found.", updateOutput.getRecords().get(0).getErrors().get(0));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -690,62 +569,101 @@ class UpdateActionTest extends BaseTest
|
|||||||
assertThat(TestUtils.queryTable(TestUtils.TABLE_NAME_ORDER)).anyMatch(r -> r.getValueString("orderNo").equals("original"));
|
assertThat(TestUtils.queryTable(TestUtils.TABLE_NAME_ORDER)).anyMatch(r -> r.getValueString("orderNo").equals("original"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
/*
|
|
||||||
@Test
|
@Test
|
||||||
void testSecurityKeyNullDenied() throws QException
|
void testSecurityKeyNullDenied() throws QException
|
||||||
{
|
{
|
||||||
QInstance qInstance = QContext.getQInstance();
|
////////////////////////////////
|
||||||
|
// insert an order in store 1 //
|
||||||
|
////////////////////////////////
|
||||||
QContext.getQSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_TYPE_STORE, 1);
|
QContext.getQSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_TYPE_STORE, 1);
|
||||||
InsertInput insertInput = new InsertInput();
|
InsertInput insertInput = new InsertInput();
|
||||||
insertInput.setTableName(TestUtils.TABLE_NAME_ORDER);
|
insertInput.setTableName(TestUtils.TABLE_NAME_ORDER);
|
||||||
insertInput.setRecords(List.of(new QRecord()));
|
insertInput.setRecords(List.of(new QRecord().withValue("id", 1).withValue("orderNo", "original").withValue("storeId", 1)));
|
||||||
InsertOutput insertOutput = new InsertAction().execute(insertInput);
|
new InsertAction().execute(insertInput);
|
||||||
assertEquals("You do not have permission to insert a record without a value in the field: Store Id", insertOutput.getRecords().get(0).getErrors().get(0));
|
|
||||||
assertEquals(0, TestUtils.queryTable(TestUtils.TABLE_NAME_ORDER).size());
|
///////////////////////////////////////////
|
||||||
|
// try to update its storeId to null now //
|
||||||
|
///////////////////////////////////////////
|
||||||
|
UpdateInput updateInput = new UpdateInput();
|
||||||
|
updateInput.setTableName(TestUtils.TABLE_NAME_ORDER);
|
||||||
|
updateInput.setRecords(List.of(new QRecord().withValue("id", 1).withValue("storeId", null)));
|
||||||
|
UpdateOutput updateOutput = new UpdateAction().execute(updateInput);
|
||||||
|
assertEquals("You do not have permission to update a record without a value in the field: Store Id", updateOutput.getRecords().get(0).getErrors().get(0));
|
||||||
|
assertEquals(0, TestUtils.queryTable(TestUtils.TABLE_NAME_ORDER).stream().filter(r -> r.getValue("storeId") == null).count());
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
/*
|
|
||||||
@Test
|
@Test
|
||||||
void testSecurityKeyNullAllowed() throws QException
|
void testSecurityKeyNullAllowed() throws QException
|
||||||
{
|
{
|
||||||
|
/////////////////////////////////////
|
||||||
|
// change storeId to be allow-null //
|
||||||
|
/////////////////////////////////////
|
||||||
QInstance qInstance = QContext.getQInstance();
|
QInstance qInstance = QContext.getQInstance();
|
||||||
qInstance.getTable(TestUtils.TABLE_NAME_ORDER).getRecordSecurityLocks().get(0).setNullValueBehavior(RecordSecurityLock.NullValueBehavior.ALLOW);
|
qInstance.getTable(TestUtils.TABLE_NAME_ORDER).getRecordSecurityLocks().get(0).setNullValueBehavior(RecordSecurityLock.NullValueBehavior.ALLOW);
|
||||||
|
|
||||||
|
////////////////////////////////
|
||||||
|
// insert an order in store 1 //
|
||||||
|
////////////////////////////////
|
||||||
QContext.getQSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_TYPE_STORE, 1);
|
QContext.getQSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_TYPE_STORE, 1);
|
||||||
InsertInput insertInput = new InsertInput();
|
InsertInput insertInput = new InsertInput();
|
||||||
insertInput.setTableName(TestUtils.TABLE_NAME_ORDER);
|
insertInput.setTableName(TestUtils.TABLE_NAME_ORDER);
|
||||||
insertInput.setRecords(List.of(new QRecord()));
|
insertInput.setRecords(List.of(new QRecord().withValue("id", 1).withValue("orderNo", "original").withValue("storeId", 1)));
|
||||||
InsertOutput insertOutput = new InsertAction().execute(insertInput);
|
new InsertAction().execute(insertInput);
|
||||||
assertEquals(0, insertOutput.getRecords().get(0).getErrors().size());
|
|
||||||
assertEquals(1, TestUtils.queryTable(TestUtils.TABLE_NAME_ORDER).size());
|
QContext.getQSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_TYPE_STORE, 1);
|
||||||
|
UpdateInput updateInput = new UpdateInput();
|
||||||
|
updateInput.setTableName(TestUtils.TABLE_NAME_ORDER);
|
||||||
|
updateInput.setRecords(List.of(new QRecord().withValue("id", 1).withValue("storeId", null)));
|
||||||
|
UpdateOutput updateOutput = new UpdateAction().execute(updateInput);
|
||||||
|
assertEquals(0, updateOutput.getRecords().get(0).getErrors().size());
|
||||||
|
assertEquals(1, TestUtils.queryTable(TestUtils.TABLE_NAME_ORDER).stream().filter(r -> r.getValue("storeId") == null).count());
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
/*
|
|
||||||
@Test
|
@Test
|
||||||
void testSecurityKeyAllAccess() throws QException
|
void testSecurityKeyAllAccess() throws QException
|
||||||
{
|
{
|
||||||
QInstance qInstance = QContext.getQInstance();
|
////////////////////////////////
|
||||||
qInstance.getTable(TestUtils.TABLE_NAME_ORDER).getRecordSecurityLocks().get(0).setNullValueBehavior(RecordSecurityLock.NullValueBehavior.ALLOW);
|
// insert 2 orders in store 1 //
|
||||||
QContext.getQSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_TYPE_STORE_ALL_ACCESS, true);
|
////////////////////////////////
|
||||||
|
QContext.getQSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_TYPE_STORE, 1);
|
||||||
InsertInput insertInput = new InsertInput();
|
InsertInput insertInput = new InsertInput();
|
||||||
insertInput.setTableName(TestUtils.TABLE_NAME_ORDER);
|
insertInput.setTableName(TestUtils.TABLE_NAME_ORDER);
|
||||||
insertInput.setRecords(List.of(
|
insertInput.setRecords(List.of(
|
||||||
new QRecord().withValue("storeId", 999),
|
new QRecord().withValue("id", 1).withValue("orderNo", "O1").withValue("storeId", 1),
|
||||||
new QRecord().withValue("storeId", null)
|
new QRecord().withValue("id", 2).withValue("orderNo", "O2").withValue("storeId", 1)
|
||||||
));
|
));
|
||||||
InsertOutput insertOutput = new InsertAction().execute(insertInput);
|
new InsertAction().execute(insertInput);
|
||||||
assertEquals(2, TestUtils.queryTable(TestUtils.TABLE_NAME_ORDER).size());
|
|
||||||
|
/////////////////////////////////////////////////////////
|
||||||
|
// make sure with all-access key we can update however //
|
||||||
|
/////////////////////////////////////////////////////////
|
||||||
|
QContext.getQSession().setSecurityKeyValues(MapBuilder.of(TestUtils.SECURITY_KEY_TYPE_STORE_ALL_ACCESS, ListBuilder.of(true)));
|
||||||
|
UpdateInput updateInput = new UpdateInput();
|
||||||
|
updateInput.setTableName(TestUtils.TABLE_NAME_ORDER);
|
||||||
|
updateInput.setRecords(List.of(
|
||||||
|
new QRecord().withValue("id", 1).withValue("storeId", 999),
|
||||||
|
new QRecord().withValue("id", 2).withValue("storeId", null)
|
||||||
|
));
|
||||||
|
UpdateOutput updateOutput = new UpdateAction().execute(updateInput);
|
||||||
|
assertEquals(0, updateOutput.getRecords().get(0).getErrors().size());
|
||||||
|
assertEquals(0, updateOutput.getRecords().get(1).getErrors().size());
|
||||||
|
assertEquals(1, TestUtils.queryTable(TestUtils.TABLE_NAME_ORDER).stream().filter(r -> Objects.equals(r.getValue("storeId"), 999)).count());
|
||||||
|
assertEquals(1, TestUtils.queryTable(TestUtils.TABLE_NAME_ORDER).stream().filter(r -> r.getValue("storeId") == null).count());
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,7 @@ package com.kingsrook.qqq.backend.core.modules.backend.implementations.utils;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
|
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
|
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
|
||||||
|
import com.kingsrook.qqq.backend.core.utils.collections.ListBuilder;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
@ -97,6 +98,21 @@ class BackendQueryFilterUtilsTest
|
|||||||
assertTrue(BackendQueryFilterUtils.doesCriteriaMatch(new QFilterCriteria("f", QCriteriaOperator.NOT_LIKE, "Test"), "f", "Tst"));
|
assertTrue(BackendQueryFilterUtils.doesCriteriaMatch(new QFilterCriteria("f", QCriteriaOperator.NOT_LIKE, "Test"), "f", "Tst"));
|
||||||
assertTrue(BackendQueryFilterUtils.doesCriteriaMatch(new QFilterCriteria("f", QCriteriaOperator.NOT_LIKE, "T%"), "f", "Rest"));
|
assertTrue(BackendQueryFilterUtils.doesCriteriaMatch(new QFilterCriteria("f", QCriteriaOperator.NOT_LIKE, "T%"), "f", "Rest"));
|
||||||
assertTrue(BackendQueryFilterUtils.doesCriteriaMatch(new QFilterCriteria("f", QCriteriaOperator.NOT_LIKE, "T_st"), "f", "Toast"));
|
assertTrue(BackendQueryFilterUtils.doesCriteriaMatch(new QFilterCriteria("f", QCriteriaOperator.NOT_LIKE, "T_st"), "f", "Toast"));
|
||||||
|
|
||||||
|
//////////////
|
||||||
|
// IN & NOT //
|
||||||
|
//////////////
|
||||||
|
assertTrue(BackendQueryFilterUtils.doesCriteriaMatch(new QFilterCriteria("f", QCriteriaOperator.IN, "A"), "f", "A"));
|
||||||
|
assertTrue(BackendQueryFilterUtils.doesCriteriaMatch(new QFilterCriteria("f", QCriteriaOperator.IN, "A", "B"), "f", "A"));
|
||||||
|
assertTrue(BackendQueryFilterUtils.doesCriteriaMatch(new QFilterCriteria("f", QCriteriaOperator.IN, "A", "B"), "f", "B"));
|
||||||
|
assertFalse(BackendQueryFilterUtils.doesCriteriaMatch(new QFilterCriteria("f", QCriteriaOperator.IN, List.of()), "f", "A"));
|
||||||
|
assertFalse(BackendQueryFilterUtils.doesCriteriaMatch(new QFilterCriteria("f", QCriteriaOperator.IN, ListBuilder.of(null)), "f", "A"));
|
||||||
|
|
||||||
|
assertFalse(BackendQueryFilterUtils.doesCriteriaMatch(new QFilterCriteria("f", QCriteriaOperator.NOT_IN, "A"), "f", "A"));
|
||||||
|
assertFalse(BackendQueryFilterUtils.doesCriteriaMatch(new QFilterCriteria("f", QCriteriaOperator.NOT_IN, "A", "B"), "f", "A"));
|
||||||
|
assertFalse(BackendQueryFilterUtils.doesCriteriaMatch(new QFilterCriteria("f", QCriteriaOperator.NOT_IN, "A", "B"), "f", "B"));
|
||||||
|
assertTrue(BackendQueryFilterUtils.doesCriteriaMatch(new QFilterCriteria("f", QCriteriaOperator.NOT_IN, List.of()), "f", "A"));
|
||||||
|
assertTrue(BackendQueryFilterUtils.doesCriteriaMatch(new QFilterCriteria("f", QCriteriaOperator.NOT_IN, ListBuilder.of(null)), "f", "A"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -619,6 +619,10 @@ public class TestUtils
|
|||||||
.withName(TABLE_NAME_ORDER_EXTRINSIC)
|
.withName(TABLE_NAME_ORDER_EXTRINSIC)
|
||||||
.withBackendName(MEMORY_BACKEND_NAME)
|
.withBackendName(MEMORY_BACKEND_NAME)
|
||||||
.withPrimaryKeyField("id")
|
.withPrimaryKeyField("id")
|
||||||
|
.withRecordSecurityLock(new RecordSecurityLock()
|
||||||
|
.withSecurityKeyType(SECURITY_KEY_TYPE_STORE)
|
||||||
|
.withFieldName("order.storeId")
|
||||||
|
.withJoinNameChain(List.of("orderOrderExtrinsic")))
|
||||||
.withField(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false))
|
.withField(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false))
|
||||||
.withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME).withIsEditable(false))
|
.withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME).withIsEditable(false))
|
||||||
.withField(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME).withIsEditable(false))
|
.withField(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME).withIsEditable(false))
|
||||||
|
@ -23,8 +23,10 @@ package com.kingsrook.qqq.backend.core.utils.collections;
|
|||||||
|
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.TreeMap;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
@ -50,6 +52,7 @@ class MapBuilderTest
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
@ -61,9 +64,22 @@ class MapBuilderTest
|
|||||||
///////////////////////////////
|
///////////////////////////////
|
||||||
Map<String, Object> map = MapBuilder.of("1", null);
|
Map<String, Object> map = MapBuilder.of("1", null);
|
||||||
|
|
||||||
|
///////////////////////////////////////
|
||||||
// this too, doesn't freaking throw. //
|
// this too, doesn't freaking throw. //
|
||||||
///////////////////////////////////////
|
///////////////////////////////////////
|
||||||
map.put("2", null);
|
map.put("2", null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
void testTypeYouRequest()
|
||||||
|
{
|
||||||
|
Map<String, Integer> myTreeMap = MapBuilder.<String, Integer>of(TreeMap::new).with("1", 1).with("2", 2).build();
|
||||||
|
assertTrue(myTreeMap instanceof TreeMap);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -92,6 +92,95 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
|
|||||||
{
|
{
|
||||||
private static final QLogger LOG = QLogger.getLogger(GenerateOpenApiSpecAction.class);
|
private static final QLogger LOG = QLogger.getLogger(GenerateOpenApiSpecAction.class);
|
||||||
|
|
||||||
|
public static final String GET_DESCRIPTION = """
|
||||||
|
Get one record from this table, by specifying its primary key as a path parameter.
|
||||||
|
""";
|
||||||
|
|
||||||
|
public static final String QUERY_DESCRIPTION = """
|
||||||
|
Execute a query on this table, using query criteria as specified in query string parameters.
|
||||||
|
|
||||||
|
* Pagination is managed via the `pageNo` & `pageSize` query string parameters. pageNo starts at 1. pageSize defaults to 50.
|
||||||
|
* By default, the response includes the total count of records that match the query criteria. The count can be omitted by specifying `includeCount=false`
|
||||||
|
* By default, results are sorted by the table's primary key, descending. This can be changed by specifying the `orderBy` query string parameter, following SQL ORDER BY syntax (e.g., `fieldName1 ASC, fieldName2 DESC`)
|
||||||
|
* By default, all given query criteria are combined using logical AND. This can be changed by specifying the query string parameter `booleanOperator=OR`.
|
||||||
|
* Each field on the table can be used as a query criteria. Each query criteria field can be specified on the query string any number of times.
|
||||||
|
* By default, all criteria use the equals operator (e.g., `myField=value` means records will be returned where myField equals value). Alternative operators can be used as follows:
|
||||||
|
* Equals: `myField=value`
|
||||||
|
* Not Equals: `myField=!value`
|
||||||
|
* Less Than: `myField=<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<>();
|
LinkedHashMap<String, String> scopes = new LinkedHashMap<>();
|
||||||
// todo, or not todo? .withScopes(scopes)
|
// todo, or not todo? .withScopes(scopes)
|
||||||
|
// seems to make a lot of "noise" on the Auth page, and for no obvious benefit...
|
||||||
securitySchemes.put("OAuth2", new OAuth2()
|
securitySchemes.put("OAuth2", new OAuth2()
|
||||||
.withFlows(MapBuilder.of("clientCredentials", new OAuth2Flow()
|
.withFlows(MapBuilder.of("clientCredentials", new OAuth2Flow()
|
||||||
.withTokenUrl("/api/oauth/token"))));
|
.withTokenUrl("/api/oauth/token"))));
|
||||||
@ -297,35 +387,20 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
|
|||||||
.withAllOf(ListBuilder.of(
|
.withAllOf(ListBuilder.of(
|
||||||
new Schema().withRef("#/components/schemas/" + tableApiName)))))));
|
new Schema().withRef("#/components/schemas/" + tableApiName)))))));
|
||||||
|
|
||||||
|
// todo...?
|
||||||
|
// includeAssociatedOrderLines=false&includeAssociatedExtrinsics=false&includeAssociatedOrderLinesExtrinsics
|
||||||
|
// includeAssociatedRecords=none
|
||||||
|
// includeAssociatedRecords=all
|
||||||
|
// includeAssociatedRecords=orderLines
|
||||||
|
// includeAssociatedRecords=orderLines,orderLines.extrinsics
|
||||||
|
// includeAssociatedRecords=extrinsics,orderLines,orderLines.extrinsics
|
||||||
|
|
||||||
//////////////////////////////////////
|
//////////////////////////////////////
|
||||||
// paths and methods for this table //
|
// paths and methods for this table //
|
||||||
//////////////////////////////////////
|
//////////////////////////////////////
|
||||||
Method queryGet = new Method()
|
Method queryGet = new Method()
|
||||||
.withSummary("Search for " + tableLabel + " records by query string")
|
.withSummary("Search for " + tableLabel + " records by query string")
|
||||||
.withDescription("""
|
.withDescription(QUERY_DESCRIPTION)
|
||||||
Execute a query on this table, using query criteria as specified in query string parameters.
|
|
||||||
|
|
||||||
* Pagination is managed via the `pageNo` & `pageSize` query string parameters. pageNo starts at 1. pageSize defaults to 50.
|
|
||||||
* By default, the response includes the total count of records that match the query criteria. The count can be omitted by specifying `includeCount=false`
|
|
||||||
* By default, results are sorted by the table's primary key, descending. This can be changed by specifying the `orderBy` query string parameter, following SQL ORDER BY syntax (e.g., `fieldName1 ASC, fieldName2 DESC`)
|
|
||||||
* By default, all given query criteria are combined using logical AND. This can be changed by specifying the query string parameter `booleanOperator=OR`.
|
|
||||||
* Each field on the table can be used as a query criteria. Each query criteria field can be specified on the query string any number of times.
|
|
||||||
* By default, all criteria use the equals operator (e.g., `myField=value` means records will be returned where myField equals value). Alternative operators can be used as follows:
|
|
||||||
* Equals: `myField=value`
|
|
||||||
* Not Equals: `myField=!value`
|
|
||||||
* Less Than: `myField=<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)
|
|
||||||
""")
|
|
||||||
.withOperationId("query" + tableApiNameUcFirst)
|
.withOperationId("query" + tableApiNameUcFirst)
|
||||||
.withTags(ListBuilder.of(tableLabel))
|
.withTags(ListBuilder.of(tableLabel))
|
||||||
.withParameters(ListBuilder.of(
|
.withParameters(ListBuilder.of(
|
||||||
@ -390,9 +465,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
|
|||||||
|
|
||||||
Method idGet = new Method()
|
Method idGet = new Method()
|
||||||
.withSummary("Get one " + tableLabel + " by " + primaryKeyLabel)
|
.withSummary("Get one " + tableLabel + " by " + primaryKeyLabel)
|
||||||
.withDescription("""
|
.withDescription(GET_DESCRIPTION)
|
||||||
Get one record from this table, by specifying its primary key as a path parameter.
|
|
||||||
""")
|
|
||||||
.withOperationId("get" + tableApiNameUcFirst)
|
.withOperationId("get" + tableApiNameUcFirst)
|
||||||
.withTags(ListBuilder.of(tableLabel))
|
.withTags(ListBuilder.of(tableLabel))
|
||||||
.withParameters(ListBuilder.of(
|
.withParameters(ListBuilder.of(
|
||||||
@ -412,16 +485,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
|
|||||||
|
|
||||||
Method idPatch = new Method()
|
Method idPatch = new Method()
|
||||||
.withSummary("Update one " + tableLabel)
|
.withSummary("Update one " + tableLabel)
|
||||||
.withDescription("""
|
.withDescription(UPDATE_DESCRIPTION)
|
||||||
Update one record in this table, by specifying its primary key as a path parameter, and by supplying values to be updated in the request body.
|
|
||||||
|
|
||||||
* Only the fields provided in the request body will be updated.
|
|
||||||
* To remove a value from a field, supply the key for the field, with a null value.
|
|
||||||
* The request body does not need to contain all fields from the table. Rather, only the fields to be updated should be supplied.
|
|
||||||
* Note that if the request body includes the primary key, it will be ignored. Only the primary key value path parameter will be used.
|
|
||||||
|
|
||||||
Upon success, a status code of 204 (`No Content`) is returned, with no response body.
|
|
||||||
""")
|
|
||||||
.withOperationId("update" + tableApiNameUcFirst)
|
.withOperationId("update" + tableApiNameUcFirst)
|
||||||
.withTags(ListBuilder.of(tableLabel))
|
.withTags(ListBuilder.of(tableLabel))
|
||||||
.withParameters(ListBuilder.of(
|
.withParameters(ListBuilder.of(
|
||||||
@ -443,11 +507,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
|
|||||||
|
|
||||||
Method idDelete = new Method()
|
Method idDelete = new Method()
|
||||||
.withSummary("Delete one " + tableLabel)
|
.withSummary("Delete one " + tableLabel)
|
||||||
.withDescription("""
|
.withDescription(DELETE_DESCRIPTION)
|
||||||
Delete one record from this table, by specifying its primary key as a path parameter.
|
|
||||||
|
|
||||||
Upon success, a status code of 204 (`No Content`) is returned, with no response body.
|
|
||||||
""")
|
|
||||||
.withOperationId("delete" + tableApiNameUcFirst)
|
.withOperationId("delete" + tableApiNameUcFirst)
|
||||||
.withTags(ListBuilder.of(tableLabel))
|
.withTags(ListBuilder.of(tableLabel))
|
||||||
.withParameters(ListBuilder.of(
|
.withParameters(ListBuilder.of(
|
||||||
@ -473,12 +533,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
|
|||||||
|
|
||||||
Method slashPost = new Method()
|
Method slashPost = new Method()
|
||||||
.withSummary("Create one " + tableLabel)
|
.withSummary("Create one " + tableLabel)
|
||||||
.withDescription("""
|
.withDescription(INSERT_DESCRIPTION)
|
||||||
Insert one record into this table by supplying the values to be inserted in the request body.
|
|
||||||
* The request body should not include a value for the table's primary key. Rather, a value will be generated and returned in a successful response's body.
|
|
||||||
|
|
||||||
Upon success, a status code of 201 (`Created`) is returned, and the generated value for the primary key will be returned in the response body object.
|
|
||||||
""")
|
|
||||||
.withRequestBody(new RequestBody()
|
.withRequestBody(new RequestBody()
|
||||||
.withRequired(true)
|
.withRequired(true)
|
||||||
.withDescription("Values for the " + tableLabel + " record to create.")
|
.withDescription("Values for the " + tableLabel + " record to create.")
|
||||||
@ -508,14 +563,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
|
|||||||
////////////////
|
////////////////
|
||||||
Method bulkPost = new Method()
|
Method bulkPost = new Method()
|
||||||
.withSummary("Create multiple " + tableLabel + " records")
|
.withSummary("Create multiple " + tableLabel + " records")
|
||||||
.withDescription("""
|
.withDescription(BULK_INSERT_DESCRIPTION)
|
||||||
Insert one or more records into this table by supplying array of records with values to be inserted, in the request body.
|
|
||||||
* The objects in the request body should not include a value for the table's primary key. Rather, a value will be generated and returned in a successful response's body
|
|
||||||
|
|
||||||
An HTTP 207 (`Multi-Status`) code is generally returned, with an array of objects giving the individual sub-status codes for each record in the request body.
|
|
||||||
* The 1st record in the request will have its response in the 1st object in the response, and so-forth.
|
|
||||||
* For sub-status codes of 201 (`Created`), and the generated value for the primary key will be returned in the response body object.
|
|
||||||
""")
|
|
||||||
.withRequestBody(new RequestBody()
|
.withRequestBody(new RequestBody()
|
||||||
.withRequired(true)
|
.withRequired(true)
|
||||||
.withDescription("Values for the " + tableLabel + " records to create.")
|
.withDescription("Values for the " + tableLabel + " records to create.")
|
||||||
@ -530,15 +578,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
|
|||||||
|
|
||||||
Method bulkPatch = new Method()
|
Method bulkPatch = new Method()
|
||||||
.withSummary("Update multiple " + tableLabel + " records")
|
.withSummary("Update multiple " + tableLabel + " records")
|
||||||
.withDescription("""
|
.withDescription(BULK_UPDATE_DESCRIPTION)
|
||||||
Update one or more records in this table, by supplying an array of records, with primary keys and values to be updated, in the request body.
|
|
||||||
* Only the fields provided in the request body will be updated.
|
|
||||||
* To remove a value from a field, supply the key for the field, with a null value.
|
|
||||||
* The request body does not need to contain all fields from the table. Rather, only the fields to be updated should be supplied.
|
|
||||||
|
|
||||||
An HTTP 207 (`Multi-Status`) code is generally returned, with an array of objects giving the individual sub-status codes for each record in the request body.
|
|
||||||
* The 1st record in the request will have its response in the 1st object in the response, and so-forth.
|
|
||||||
""")
|
|
||||||
.withRequestBody(new RequestBody()
|
.withRequestBody(new RequestBody()
|
||||||
.withRequired(true)
|
.withRequired(true)
|
||||||
.withDescription("Values for the " + tableLabel + " records to update.")
|
.withDescription("Values for the " + tableLabel + " records to update.")
|
||||||
@ -559,12 +599,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
|
|||||||
|
|
||||||
Method bulkDelete = new Method()
|
Method bulkDelete = new Method()
|
||||||
.withSummary("Delete multiple " + tableLabel + " records")
|
.withSummary("Delete multiple " + tableLabel + " records")
|
||||||
.withDescription("""
|
.withDescription(BULK_DELETE_DESCRIPTION)
|
||||||
Delete one or more records from this table, by supplying an array of primary key values in the request body.
|
|
||||||
|
|
||||||
An HTTP 207 (`Multi-Status`) code is generally returned, with an array of objects giving the individual sub-status codes for each record in the request body.
|
|
||||||
* The 1st primary key in the request will have its response in the 1st object in the response, and so-forth.
|
|
||||||
""")
|
|
||||||
.withRequestBody(new RequestBody()
|
.withRequestBody(new RequestBody()
|
||||||
.withRequired(true)
|
.withRequired(true)
|
||||||
.withDescription(primaryKeyLabel + " values for the " + tableLabel + " records to delete.")
|
.withDescription(primaryKeyLabel + " values for the " + tableLabel + " records to delete.")
|
||||||
@ -835,20 +870,34 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
|
|||||||
case "patch" -> ListBuilder.of(
|
case "patch" -> ListBuilder.of(
|
||||||
MapBuilder.of(LinkedHashMap::new)
|
MapBuilder.of(LinkedHashMap::new)
|
||||||
.with("statusCode", HttpStatus.NO_CONTENT.getCode())
|
.with("statusCode", HttpStatus.NO_CONTENT.getCode())
|
||||||
.with("statusText", HttpStatus.NO_CONTENT.getMessage()).build(),
|
.with("statusText", HttpStatus.NO_CONTENT.getMessage())
|
||||||
|
.with(primaryKeyApiName, "47").build(),
|
||||||
MapBuilder.of(LinkedHashMap::new)
|
MapBuilder.of(LinkedHashMap::new)
|
||||||
.with("statusCode", HttpStatus.BAD_REQUEST.getCode())
|
.with("statusCode", HttpStatus.BAD_REQUEST.getCode())
|
||||||
.with("statusText", HttpStatus.BAD_REQUEST.getMessage())
|
.with("statusText", HttpStatus.BAD_REQUEST.getMessage())
|
||||||
.with("error", "Could not update " + tableLabel + ": Duplicate value in unique key field.").build()
|
.with("error", "Could not update " + tableLabel + ": Missing value in required field: My Field.")
|
||||||
|
.with(primaryKeyApiName, "47").build(),
|
||||||
|
MapBuilder.of(LinkedHashMap::new)
|
||||||
|
.with("statusCode", HttpStatus.NOT_FOUND.getCode())
|
||||||
|
.with("statusText", HttpStatus.NOT_FOUND.getMessage())
|
||||||
|
.with("error", "The requested " + tableLabel + " to update was not found.")
|
||||||
|
.with(primaryKeyApiName, "47").build()
|
||||||
);
|
);
|
||||||
case "delete" -> ListBuilder.of(
|
case "delete" -> ListBuilder.of(
|
||||||
MapBuilder.of(LinkedHashMap::new)
|
MapBuilder.of(LinkedHashMap::new)
|
||||||
.with("statusCode", HttpStatus.NO_CONTENT.getCode())
|
.with("statusCode", HttpStatus.NO_CONTENT.getCode())
|
||||||
.with("statusText", HttpStatus.NO_CONTENT.getMessage()).build(),
|
.with("statusText", HttpStatus.NO_CONTENT.getMessage())
|
||||||
|
.with(primaryKeyApiName, "47").build(),
|
||||||
MapBuilder.of(LinkedHashMap::new)
|
MapBuilder.of(LinkedHashMap::new)
|
||||||
.with("statusCode", HttpStatus.BAD_REQUEST.getCode())
|
.with("statusCode", HttpStatus.BAD_REQUEST.getCode())
|
||||||
.with("statusText", HttpStatus.BAD_REQUEST.getMessage())
|
.with("statusText", HttpStatus.BAD_REQUEST.getMessage())
|
||||||
.with("error", "Could not delete " + tableLabel + ": Foreign key constraint violation.").build()
|
.with("error", "Could not delete " + tableLabel + ": Foreign key constraint violation.")
|
||||||
|
.with(primaryKeyApiName, "47").build(),
|
||||||
|
MapBuilder.of(LinkedHashMap::new)
|
||||||
|
.with("statusCode", HttpStatus.NOT_FOUND.getCode())
|
||||||
|
.with("statusText", HttpStatus.NOT_FOUND.getMessage())
|
||||||
|
.with("error", "The requested " + tableLabel + " to delete was not found.")
|
||||||
|
.with(primaryKeyApiName, "47").build()
|
||||||
);
|
);
|
||||||
default -> throw (new IllegalArgumentException("Unrecognized method: " + method));
|
default -> throw (new IllegalArgumentException("Unrecognized method: " + method));
|
||||||
};
|
};
|
||||||
@ -857,10 +906,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
|
|||||||
properties.put("statusCode", new Schema().withType("integer"));
|
properties.put("statusCode", new Schema().withType("integer"));
|
||||||
properties.put("statusText", new Schema().withType("string"));
|
properties.put("statusText", new Schema().withType("string"));
|
||||||
properties.put("error", new Schema().withType("string"));
|
properties.put("error", new Schema().withType("string"));
|
||||||
if(method.equalsIgnoreCase("post"))
|
|
||||||
{
|
|
||||||
properties.put(primaryKeyApiName, new Schema().withType(getFieldType(primaryKeyField)));
|
properties.put(primaryKeyApiName, new Schema().withType(getFieldType(primaryKeyField)));
|
||||||
}
|
|
||||||
|
|
||||||
return new Response()
|
return new Response()
|
||||||
.withDescription("Multiple statuses. See body for details.")
|
.withDescription("Multiple statuses. See body for details.")
|
||||||
@ -870,9 +916,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
|
|||||||
.withItems(new Schema()
|
.withItems(new Schema()
|
||||||
.withType("object")
|
.withType("object")
|
||||||
.withProperties(properties))
|
.withProperties(properties))
|
||||||
.withExample(example)
|
.withExample(example))));
|
||||||
)
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -34,6 +34,7 @@ import com.kingsrook.qqq.api.model.actions.GetTableApiFieldsInput;
|
|||||||
import com.kingsrook.qqq.api.model.metadata.fields.ApiFieldMetaData;
|
import com.kingsrook.qqq.api.model.metadata.fields.ApiFieldMetaData;
|
||||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||||
|
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.Association;
|
import com.kingsrook.qqq.backend.core.model.metadata.tables.Association;
|
||||||
@ -50,6 +51,8 @@ import org.json.JSONObject;
|
|||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public class QRecordApiAdapter
|
public class QRecordApiAdapter
|
||||||
{
|
{
|
||||||
|
private static final QLogger LOG = QLogger.getLogger(QRecordApiAdapter.class);
|
||||||
|
|
||||||
private static Map<Pair<String, String>, List<QFieldMetaData>> fieldListCache = new HashMap<>();
|
private static Map<Pair<String, String>, List<QFieldMetaData>> fieldListCache = new HashMap<>();
|
||||||
private static Map<Pair<String, String>, Map<String, QFieldMetaData>> fieldMapCache = new HashMap<>();
|
private static Map<Pair<String, String>, Map<String, QFieldMetaData>> fieldMapCache = new HashMap<>();
|
||||||
|
|
||||||
@ -105,7 +108,7 @@ public class QRecordApiAdapter
|
|||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public static QRecord apiJsonObjectToQRecord(JSONObject jsonObject, String tableName, String apiVersion) throws QException
|
public static QRecord apiJsonObjectToQRecord(JSONObject jsonObject, String tableName, String apiVersion, boolean includePrimaryKey) throws QException
|
||||||
{
|
{
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
// make map of apiFieldNames (e.g., names as api uses them) to QFieldMetaData //
|
// make map of apiFieldNames (e.g., names as api uses them) to QFieldMetaData //
|
||||||
@ -134,6 +137,22 @@ public class QRecordApiAdapter
|
|||||||
QFieldMetaData field = apiFieldsMap.get(jsonKey);
|
QFieldMetaData field = apiFieldsMap.get(jsonKey);
|
||||||
Object value = jsonObject.isNull(jsonKey) ? null : jsonObject.get(jsonKey);
|
Object value = jsonObject.isNull(jsonKey) ? null : jsonObject.get(jsonKey);
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// generally, omit non-editable fields - //
|
||||||
|
// however - if we're asked to include the primary key (and this is the primary key), then include it //
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
if(!field.getIsEditable())
|
||||||
|
{
|
||||||
|
if(includePrimaryKey && field.getName().equals(table.getPrimaryKeyField()))
|
||||||
|
{
|
||||||
|
LOG.trace("Even though field [" + field.getName() + "] is not editable, we'll use it, because it's the primary key, and we've been asked to include primary keys");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ApiFieldMetaData apiFieldMetaData = ApiFieldMetaData.of(field);
|
ApiFieldMetaData apiFieldMetaData = ApiFieldMetaData.of(field);
|
||||||
if(StringUtils.hasContent(apiFieldMetaData.getReplacedByFieldName()))
|
if(StringUtils.hasContent(apiFieldMetaData.getReplacedByFieldName()))
|
||||||
{
|
{
|
||||||
@ -146,6 +165,11 @@ public class QRecordApiAdapter
|
|||||||
}
|
}
|
||||||
else if(associationMap.containsKey(jsonKey))
|
else if(associationMap.containsKey(jsonKey))
|
||||||
{
|
{
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// else, if it's an association - process that (recursively as a list of other records) //
|
||||||
|
// todo - should probably define in meta-data if an association is included in the api or not!! //
|
||||||
|
// and what its name is too... //
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
Association association = associationMap.get(jsonKey);
|
Association association = associationMap.get(jsonKey);
|
||||||
Object value = jsonObject.get(jsonKey);
|
Object value = jsonObject.get(jsonKey);
|
||||||
if(value instanceof JSONArray jsonArray)
|
if(value instanceof JSONArray jsonArray)
|
||||||
@ -154,7 +178,7 @@ public class QRecordApiAdapter
|
|||||||
{
|
{
|
||||||
if(subObject instanceof JSONObject subJsonObject)
|
if(subObject instanceof JSONObject subJsonObject)
|
||||||
{
|
{
|
||||||
QRecord subRecord = apiJsonObjectToQRecord(subJsonObject, association.getAssociatedTableName(), apiVersion);
|
QRecord subRecord = apiJsonObjectToQRecord(subJsonObject, association.getAssociatedTableName(), apiVersion, includePrimaryKey);
|
||||||
qRecord.withAssociatedRecord(association.getName(), subRecord);
|
qRecord.withAssociatedRecord(association.getName(), subRecord);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
@ -1066,7 +1066,7 @@ public class QJavalinApiHandler
|
|||||||
JSONTokener jsonTokener = new JSONTokener(context.body().trim());
|
JSONTokener jsonTokener = new JSONTokener(context.body().trim());
|
||||||
JSONObject jsonObject = new JSONObject(jsonTokener);
|
JSONObject jsonObject = new JSONObject(jsonTokener);
|
||||||
|
|
||||||
insertInput.setRecords(List.of(QRecordApiAdapter.apiJsonObjectToQRecord(jsonObject, tableName, version)));
|
insertInput.setRecords(List.of(QRecordApiAdapter.apiJsonObjectToQRecord(jsonObject, tableName, version, false)));
|
||||||
|
|
||||||
if(jsonTokener.more())
|
if(jsonTokener.more())
|
||||||
{
|
{
|
||||||
@ -1142,7 +1142,7 @@ public class QJavalinApiHandler
|
|||||||
for(int i = 0; i < jsonArray.length(); i++)
|
for(int i = 0; i < jsonArray.length(); i++)
|
||||||
{
|
{
|
||||||
JSONObject jsonObject = jsonArray.getJSONObject(i);
|
JSONObject jsonObject = jsonArray.getJSONObject(i);
|
||||||
recordList.add(QRecordApiAdapter.apiJsonObjectToQRecord(jsonObject, tableName, version));
|
recordList.add(QRecordApiAdapter.apiJsonObjectToQRecord(jsonObject, tableName, version, false));
|
||||||
}
|
}
|
||||||
|
|
||||||
if(jsonTokener.more())
|
if(jsonTokener.more())
|
||||||
@ -1248,7 +1248,7 @@ public class QJavalinApiHandler
|
|||||||
for(int i = 0; i < jsonArray.length(); i++)
|
for(int i = 0; i < jsonArray.length(); i++)
|
||||||
{
|
{
|
||||||
JSONObject jsonObject = jsonArray.getJSONObject(i);
|
JSONObject jsonObject = jsonArray.getJSONObject(i);
|
||||||
recordList.add(QRecordApiAdapter.apiJsonObjectToQRecord(jsonObject, tableName, version));
|
recordList.add(QRecordApiAdapter.apiJsonObjectToQRecord(jsonObject, tableName, version, true));
|
||||||
}
|
}
|
||||||
|
|
||||||
if(jsonTokener.more())
|
if(jsonTokener.more())
|
||||||
@ -1280,23 +1280,47 @@ public class QJavalinApiHandler
|
|||||||
// process records to build response //
|
// process records to build response //
|
||||||
///////////////////////////////////////
|
///////////////////////////////////////
|
||||||
List<Map<String, Serializable>> response = new ArrayList<>();
|
List<Map<String, Serializable>> response = new ArrayList<>();
|
||||||
|
int i = 0;
|
||||||
for(QRecord record : updateOutput.getRecords())
|
for(QRecord record : updateOutput.getRecords())
|
||||||
{
|
{
|
||||||
LinkedHashMap<String, Serializable> outputRecord = new LinkedHashMap<>();
|
LinkedHashMap<String, Serializable> outputRecord = new LinkedHashMap<>();
|
||||||
response.add(outputRecord);
|
response.add(outputRecord);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
QRecord inputRecord = updateInput.getRecords().get(i);
|
||||||
|
Serializable primaryKey = inputRecord.getValue(table.getPrimaryKeyField());
|
||||||
|
outputRecord.put(table.getPrimaryKeyField(), primaryKey);
|
||||||
|
}
|
||||||
|
catch(Exception e)
|
||||||
|
{
|
||||||
|
//////////
|
||||||
|
// omit //
|
||||||
|
//////////
|
||||||
|
}
|
||||||
|
|
||||||
List<String> errors = record.getErrors();
|
List<String> errors = record.getErrors();
|
||||||
if(CollectionUtils.nullSafeHasContents(errors))
|
if(CollectionUtils.nullSafeHasContents(errors))
|
||||||
|
{
|
||||||
|
outputRecord.put("error", "Error updating " + table.getLabel() + ": " + StringUtils.joinWithCommasAndAnd(errors));
|
||||||
|
if(areAnyErrorsNotFound(errors))
|
||||||
|
{
|
||||||
|
outputRecord.put("statusCode", HttpStatus.Code.NOT_FOUND.getCode());
|
||||||
|
outputRecord.put("statusText", HttpStatus.Code.NOT_FOUND.getMessage());
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
outputRecord.put("statusCode", HttpStatus.Code.BAD_REQUEST.getCode());
|
outputRecord.put("statusCode", HttpStatus.Code.BAD_REQUEST.getCode());
|
||||||
outputRecord.put("statusText", HttpStatus.Code.BAD_REQUEST.getMessage());
|
outputRecord.put("statusText", HttpStatus.Code.BAD_REQUEST.getMessage());
|
||||||
outputRecord.put("error", "Error updating " + table.getLabel() + ": " + StringUtils.joinWithCommasAndAnd(errors));
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
outputRecord.put("statusCode", HttpStatus.Code.NO_CONTENT.getCode());
|
outputRecord.put("statusCode", HttpStatus.Code.NO_CONTENT.getCode());
|
||||||
outputRecord.put("statusText", HttpStatus.Code.NO_CONTENT.getMessage());
|
outputRecord.put("statusText", HttpStatus.Code.NO_CONTENT.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
i++;
|
||||||
}
|
}
|
||||||
|
|
||||||
QJavalinAccessLogger.logEndSuccess();
|
QJavalinAccessLogger.logEndSuccess();
|
||||||
@ -1312,6 +1336,16 @@ public class QJavalinApiHandler
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
private static boolean areAnyErrorsNotFound(List<String> errors)
|
||||||
|
{
|
||||||
|
return errors.stream().anyMatch(e -> e.startsWith(UpdateAction.NOT_FOUND_ERROR_PREFIX) || e.startsWith(DeleteAction.NOT_FOUND_ERROR_PREFIX));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
@ -1391,24 +1425,34 @@ public class QJavalinApiHandler
|
|||||||
List<Map<String, Serializable>> response = new ArrayList<>();
|
List<Map<String, Serializable>> response = new ArrayList<>();
|
||||||
|
|
||||||
List<QRecord> recordsWithErrors = deleteOutput.getRecordsWithErrors();
|
List<QRecord> recordsWithErrors = deleteOutput.getRecordsWithErrors();
|
||||||
Map<String, String> primaryKeyToErrorMap = new HashMap<>();
|
Map<String, List<String>> primaryKeyToErrorsMap = new HashMap<>();
|
||||||
for(QRecord recordWithError : CollectionUtils.nonNullList(recordsWithErrors))
|
for(QRecord recordWithError : CollectionUtils.nonNullList(recordsWithErrors))
|
||||||
{
|
{
|
||||||
String primaryKey = recordWithError.getValueString(table.getPrimaryKeyField());
|
String primaryKey = recordWithError.getValueString(table.getPrimaryKeyField());
|
||||||
primaryKeyToErrorMap.put(primaryKey, StringUtils.join(", ", recordWithError.getErrors()));
|
primaryKeyToErrorsMap.put(primaryKey, recordWithError.getErrors());
|
||||||
}
|
}
|
||||||
|
|
||||||
for(Serializable primaryKey : deleteInput.getPrimaryKeys())
|
for(Serializable primaryKey : deleteInput.getPrimaryKeys())
|
||||||
{
|
{
|
||||||
LinkedHashMap<String, Serializable> outputRecord = new LinkedHashMap<>();
|
LinkedHashMap<String, Serializable> outputRecord = new LinkedHashMap<>();
|
||||||
response.add(outputRecord);
|
response.add(outputRecord);
|
||||||
|
outputRecord.put(table.getPrimaryKeyField(), primaryKey);
|
||||||
|
|
||||||
String primaryKeyString = ValueUtils.getValueAsString((primaryKey));
|
String primaryKeyString = ValueUtils.getValueAsString(primaryKey);
|
||||||
if(primaryKeyToErrorMap.containsKey(primaryKeyString))
|
List<String> errors = primaryKeyToErrorsMap.get(primaryKeyString);
|
||||||
|
if(CollectionUtils.nullSafeHasContents(errors))
|
||||||
|
{
|
||||||
|
outputRecord.put("error", "Error deleting " + table.getLabel() + ": " + StringUtils.joinWithCommasAndAnd(errors));
|
||||||
|
if(areAnyErrorsNotFound(errors))
|
||||||
|
{
|
||||||
|
outputRecord.put("statusCode", HttpStatus.Code.NOT_FOUND.getCode());
|
||||||
|
outputRecord.put("statusText", HttpStatus.Code.NOT_FOUND.getMessage());
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
outputRecord.put("statusCode", HttpStatus.Code.BAD_REQUEST.getCode());
|
outputRecord.put("statusCode", HttpStatus.Code.BAD_REQUEST.getCode());
|
||||||
outputRecord.put("statusText", HttpStatus.Code.BAD_REQUEST.getMessage());
|
outputRecord.put("statusText", HttpStatus.Code.BAD_REQUEST.getMessage());
|
||||||
outputRecord.put("error", "Error deleting " + table.getLabel() + ": " + primaryKeyToErrorMap.get(primaryKeyString));
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -1453,20 +1497,6 @@ public class QJavalinApiHandler
|
|||||||
|
|
||||||
PermissionsHelper.checkTablePermissionThrowing(updateInput, TablePermissionSubType.EDIT);
|
PermissionsHelper.checkTablePermissionThrowing(updateInput, TablePermissionSubType.EDIT);
|
||||||
|
|
||||||
///////////////////////////////////////////////////////
|
|
||||||
// throw a not found error if the record isn't found //
|
|
||||||
///////////////////////////////////////////////////////
|
|
||||||
GetInput getInput = new GetInput();
|
|
||||||
getInput.setTableName(tableName);
|
|
||||||
getInput.setPrimaryKey(primaryKey);
|
|
||||||
GetAction getAction = new GetAction();
|
|
||||||
GetOutput getOutput = getAction.execute(getInput);
|
|
||||||
if(getOutput.getRecord() == null)
|
|
||||||
{
|
|
||||||
throw (new QNotFoundException("Could not find " + table.getLabel() + " with "
|
|
||||||
+ table.getFields().get(table.getPrimaryKeyField()).getLabel() + " of " + primaryKey));
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if(!StringUtils.hasContent(context.body()))
|
if(!StringUtils.hasContent(context.body()))
|
||||||
@ -1477,7 +1507,7 @@ public class QJavalinApiHandler
|
|||||||
JSONTokener jsonTokener = new JSONTokener(context.body().trim());
|
JSONTokener jsonTokener = new JSONTokener(context.body().trim());
|
||||||
JSONObject jsonObject = new JSONObject(jsonTokener);
|
JSONObject jsonObject = new JSONObject(jsonTokener);
|
||||||
|
|
||||||
QRecord qRecord = QRecordApiAdapter.apiJsonObjectToQRecord(jsonObject, tableName, version);
|
QRecord qRecord = QRecordApiAdapter.apiJsonObjectToQRecord(jsonObject, tableName, version, false);
|
||||||
qRecord.setValue(table.getPrimaryKeyField(), primaryKey);
|
qRecord.setValue(table.getPrimaryKeyField(), primaryKey);
|
||||||
updateInput.setRecords(List.of(qRecord));
|
updateInput.setRecords(List.of(qRecord));
|
||||||
|
|
||||||
@ -1501,8 +1531,18 @@ public class QJavalinApiHandler
|
|||||||
List<String> errors = updateOutput.getRecords().get(0).getErrors();
|
List<String> errors = updateOutput.getRecords().get(0).getErrors();
|
||||||
if(CollectionUtils.nullSafeHasContents(errors))
|
if(CollectionUtils.nullSafeHasContents(errors))
|
||||||
{
|
{
|
||||||
|
if(areAnyErrorsNotFound(errors))
|
||||||
|
{
|
||||||
|
throw (new QNotFoundException("Could not find " + table.getLabel() + " with " + table.getFields().get(table.getPrimaryKeyField()).getLabel() + " of " + primaryKey));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// todo - could be smarter here, about some of these errors being 400, not 500... e.g., a missing required field //
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
throw (new QException("Error updating " + table.getLabel() + ": " + StringUtils.joinWithCommasAndAnd(errors)));
|
throw (new QException("Error updating " + table.getLabel() + ": " + StringUtils.joinWithCommasAndAnd(errors)));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
QJavalinAccessLogger.logEndSuccess();
|
QJavalinAccessLogger.logEndSuccess();
|
||||||
context.status(HttpStatus.Code.NO_CONTENT.getCode());
|
context.status(HttpStatus.Code.NO_CONTENT.getCode());
|
||||||
@ -1540,29 +1580,22 @@ public class QJavalinApiHandler
|
|||||||
|
|
||||||
PermissionsHelper.checkTablePermissionThrowing(deleteInput, TablePermissionSubType.DELETE);
|
PermissionsHelper.checkTablePermissionThrowing(deleteInput, TablePermissionSubType.DELETE);
|
||||||
|
|
||||||
///////////////////////////////////////////////////////
|
|
||||||
// throw a not found error if the record isn't found //
|
|
||||||
///////////////////////////////////////////////////////
|
|
||||||
GetInput getInput = new GetInput();
|
|
||||||
getInput.setTableName(tableName);
|
|
||||||
getInput.setPrimaryKey(primaryKey);
|
|
||||||
GetAction getAction = new GetAction();
|
|
||||||
GetOutput getOutput = getAction.execute(getInput);
|
|
||||||
if(getOutput.getRecord() == null)
|
|
||||||
{
|
|
||||||
throw (new QNotFoundException("Could not find " + table.getLabel() + " with "
|
|
||||||
+ table.getFields().get(table.getPrimaryKeyField()).getLabel() + " of " + primaryKey));
|
|
||||||
}
|
|
||||||
|
|
||||||
///////////////////
|
///////////////////
|
||||||
// do the delete //
|
// do the delete //
|
||||||
///////////////////
|
///////////////////
|
||||||
DeleteAction deleteAction = new DeleteAction();
|
DeleteAction deleteAction = new DeleteAction();
|
||||||
DeleteOutput deleteOutput = deleteAction.execute(deleteInput);
|
DeleteOutput deleteOutput = deleteAction.execute(deleteInput);
|
||||||
if(CollectionUtils.nullSafeHasContents(deleteOutput.getRecordsWithErrors()))
|
if(CollectionUtils.nullSafeHasContents(deleteOutput.getRecordsWithErrors()))
|
||||||
|
{
|
||||||
|
if(areAnyErrorsNotFound(deleteOutput.getRecordsWithErrors().get(0).getErrors()))
|
||||||
|
{
|
||||||
|
throw (new QNotFoundException("Could not find " + table.getLabel() + " with " + table.getFields().get(table.getPrimaryKeyField()).getLabel() + " of " + primaryKey));
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
throw (new QException("Error deleting " + table.getLabel() + ": " + StringUtils.joinWithCommasAndAnd(deleteOutput.getRecordsWithErrors().get(0).getErrors())));
|
throw (new QException("Error deleting " + table.getLabel() + ": " + StringUtils.joinWithCommasAndAnd(deleteOutput.getRecordsWithErrors().get(0).getErrors())));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
QJavalinAccessLogger.logEndSuccess();
|
QJavalinAccessLogger.logEndSuccess();
|
||||||
context.status(HttpStatus.Code.NO_CONTENT.getCode());
|
context.status(HttpStatus.Code.NO_CONTENT.getCode());
|
||||||
|
@ -97,7 +97,7 @@ class QRecordApiAdapterTest extends BaseTest
|
|||||||
///////////////////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
QRecord recordFromOldApi = QRecordApiAdapter.apiJsonObjectToQRecord(new JSONObject("""
|
QRecord recordFromOldApi = QRecordApiAdapter.apiJsonObjectToQRecord(new JSONObject("""
|
||||||
{"firstName": "Tim", "shoeCount": 2}
|
{"firstName": "Tim", "shoeCount": 2}
|
||||||
"""), TestUtils.TABLE_NAME_PERSON, TestUtils.V2022_Q4);
|
"""), TestUtils.TABLE_NAME_PERSON, TestUtils.V2022_Q4, true);
|
||||||
assertEquals(2, recordFromOldApi.getValueInteger("noOfShoes"));
|
assertEquals(2, recordFromOldApi.getValueInteger("noOfShoes"));
|
||||||
|
|
||||||
///////////////////////////////////////////
|
///////////////////////////////////////////
|
||||||
@ -105,7 +105,7 @@ class QRecordApiAdapterTest extends BaseTest
|
|||||||
///////////////////////////////////////////
|
///////////////////////////////////////////
|
||||||
QRecord recordFromCurrentApi = QRecordApiAdapter.apiJsonObjectToQRecord(new JSONObject("""
|
QRecord recordFromCurrentApi = QRecordApiAdapter.apiJsonObjectToQRecord(new JSONObject("""
|
||||||
{"firstName": "Tim", "noOfShoes": 2}
|
{"firstName": "Tim", "noOfShoes": 2}
|
||||||
"""), TestUtils.TABLE_NAME_PERSON, TestUtils.V2023_Q1);
|
"""), TestUtils.TABLE_NAME_PERSON, TestUtils.V2023_Q1, true);
|
||||||
assertEquals(2, recordFromCurrentApi.getValueInteger("noOfShoes"));
|
assertEquals(2, recordFromCurrentApi.getValueInteger("noOfShoes"));
|
||||||
|
|
||||||
/////////////////////////////////////////////
|
/////////////////////////////////////////////
|
||||||
@ -113,7 +113,7 @@ class QRecordApiAdapterTest extends BaseTest
|
|||||||
/////////////////////////////////////////////
|
/////////////////////////////////////////////
|
||||||
QRecord recordFromFutureApi = QRecordApiAdapter.apiJsonObjectToQRecord(new JSONObject("""
|
QRecord recordFromFutureApi = QRecordApiAdapter.apiJsonObjectToQRecord(new JSONObject("""
|
||||||
{"firstName": "Tim", "noOfShoes": 2, "cost": 3.50}
|
{"firstName": "Tim", "noOfShoes": 2, "cost": 3.50}
|
||||||
"""), TestUtils.TABLE_NAME_PERSON, TestUtils.V2023_Q2);
|
"""), TestUtils.TABLE_NAME_PERSON, TestUtils.V2023_Q2, true);
|
||||||
assertEquals(2, recordFromFutureApi.getValueInteger("noOfShoes"));
|
assertEquals(2, recordFromFutureApi.getValueInteger("noOfShoes"));
|
||||||
assertEquals(new BigDecimal("3.50"), recordFromFutureApi.getValueBigDecimal("cost"));
|
assertEquals(new BigDecimal("3.50"), recordFromFutureApi.getValueBigDecimal("cost"));
|
||||||
|
|
||||||
@ -122,7 +122,7 @@ class QRecordApiAdapterTest extends BaseTest
|
|||||||
///////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////
|
||||||
QRecord recordWithApiFieldName = QRecordApiAdapter.apiJsonObjectToQRecord(new JSONObject("""
|
QRecord recordWithApiFieldName = QRecordApiAdapter.apiJsonObjectToQRecord(new JSONObject("""
|
||||||
{"firstName": "Tim", "birthDay": "1976-05-28"}
|
{"firstName": "Tim", "birthDay": "1976-05-28"}
|
||||||
"""), TestUtils.TABLE_NAME_PERSON, TestUtils.V2023_Q2);
|
"""), TestUtils.TABLE_NAME_PERSON, TestUtils.V2023_Q2, true);
|
||||||
assertEquals("1976-05-28", recordWithApiFieldName.getValueString("birthDate"));
|
assertEquals("1976-05-28", recordWithApiFieldName.getValueString("birthDate"));
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
@ -130,7 +130,7 @@ class QRecordApiAdapterTest extends BaseTest
|
|||||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
assertThatThrownBy(() -> QRecordApiAdapter.apiJsonObjectToQRecord(new JSONObject("""
|
assertThatThrownBy(() -> QRecordApiAdapter.apiJsonObjectToQRecord(new JSONObject("""
|
||||||
{"firstName": "Tim", "noOfShoes": 2}
|
{"firstName": "Tim", "noOfShoes": 2}
|
||||||
"""), TestUtils.TABLE_NAME_PERSON, TestUtils.V2022_Q4))
|
"""), TestUtils.TABLE_NAME_PERSON, TestUtils.V2022_Q4, true))
|
||||||
.isInstanceOf(QBadRequestException.class)
|
.isInstanceOf(QBadRequestException.class)
|
||||||
.hasMessageContaining("unrecognized field name: noOfShoes");
|
.hasMessageContaining("unrecognized field name: noOfShoes");
|
||||||
|
|
||||||
@ -139,7 +139,7 @@ class QRecordApiAdapterTest extends BaseTest
|
|||||||
/////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////
|
||||||
assertThatThrownBy(() -> QRecordApiAdapter.apiJsonObjectToQRecord(new JSONObject("""
|
assertThatThrownBy(() -> QRecordApiAdapter.apiJsonObjectToQRecord(new JSONObject("""
|
||||||
{"firstName": "Tim", "cost": 2}
|
{"firstName": "Tim", "cost": 2}
|
||||||
"""), TestUtils.TABLE_NAME_PERSON, TestUtils.V2023_Q1))
|
"""), TestUtils.TABLE_NAME_PERSON, TestUtils.V2023_Q1, true))
|
||||||
.isInstanceOf(QBadRequestException.class)
|
.isInstanceOf(QBadRequestException.class)
|
||||||
.hasMessageContaining("unrecognized field name: cost");
|
.hasMessageContaining("unrecognized field name: cost");
|
||||||
|
|
||||||
@ -150,11 +150,29 @@ class QRecordApiAdapterTest extends BaseTest
|
|||||||
{
|
{
|
||||||
assertThatThrownBy(() -> QRecordApiAdapter.apiJsonObjectToQRecord(new JSONObject("""
|
assertThatThrownBy(() -> QRecordApiAdapter.apiJsonObjectToQRecord(new JSONObject("""
|
||||||
{"firstName": "Tim", "price": 2}
|
{"firstName": "Tim", "price": 2}
|
||||||
"""), TestUtils.TABLE_NAME_PERSON, version))
|
"""), TestUtils.TABLE_NAME_PERSON, version, true))
|
||||||
.isInstanceOf(QBadRequestException.class)
|
.isInstanceOf(QBadRequestException.class)
|
||||||
.hasMessageContaining("unrecognized field name: price");
|
.hasMessageContaining("unrecognized field name: price");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////
|
||||||
|
// assert non-editable fields are omitted //
|
||||||
|
////////////////////////////////////////////
|
||||||
|
QRecord recordWithoutNonEditableFields = QRecordApiAdapter.apiJsonObjectToQRecord(new JSONObject("""
|
||||||
|
{"firstName": "Tim", "birthDay": "1976-05-28", "createDate": "2023-03-31T11:44:28Z", "id": 256}
|
||||||
|
"""), TestUtils.TABLE_NAME_PERSON, TestUtils.V2023_Q1, false);
|
||||||
|
assertFalse(recordWithoutNonEditableFields.getValues().containsKey("createDate"));
|
||||||
|
assertFalse(recordWithoutNonEditableFields.getValues().containsKey("id"));
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////
|
||||||
|
// assert non-editable primary key fields IS included, if so requested //
|
||||||
|
/////////////////////////////////////////////////////////////////////////
|
||||||
|
QRecord recordWithoutNonEditablePrimaryKeyFields = QRecordApiAdapter.apiJsonObjectToQRecord(new JSONObject("""
|
||||||
|
{"firstName": "Tim", "birthDay": "1976-05-28", "createDate": "2023-03-31T11:44:28Z", "id": 256}
|
||||||
|
"""), TestUtils.TABLE_NAME_PERSON, TestUtils.V2023_Q1, true);
|
||||||
|
assertFalse(recordWithoutNonEditablePrimaryKeyFields.getValues().containsKey("createDate"));
|
||||||
|
assertEquals(256, recordWithoutNonEditablePrimaryKeyFields.getValues().get("id"));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -796,19 +796,29 @@ class QJavalinApiHandlerTest extends BaseTest
|
|||||||
[
|
[
|
||||||
{"id": 1, "email": "homer@simpson.com"},
|
{"id": 1, "email": "homer@simpson.com"},
|
||||||
{"id": 2, "email": "marge@simpson.com"},
|
{"id": 2, "email": "marge@simpson.com"},
|
||||||
{"email": "nobody@simpson.com"}
|
{"email": "nobody@simpson.com"},
|
||||||
|
{"id": 256, "email": "256@simpson.com"}
|
||||||
]
|
]
|
||||||
""")
|
""")
|
||||||
.asString();
|
.asString();
|
||||||
assertEquals(HttpStatus.MULTI_STATUS_207, response.getStatus());
|
assertEquals(HttpStatus.MULTI_STATUS_207, response.getStatus());
|
||||||
JSONArray jsonArray = new JSONArray(response.getBody());
|
JSONArray jsonArray = new JSONArray(response.getBody());
|
||||||
assertEquals(3, jsonArray.length());
|
System.out.println(jsonArray.toString(3));
|
||||||
|
assertEquals(4, jsonArray.length());
|
||||||
|
|
||||||
assertEquals(HttpStatus.NO_CONTENT_204, jsonArray.getJSONObject(0).getInt("statusCode"));
|
assertEquals(HttpStatus.NO_CONTENT_204, jsonArray.getJSONObject(0).getInt("statusCode"));
|
||||||
|
assertEquals(1, jsonArray.getJSONObject(0).getInt("id"));
|
||||||
|
|
||||||
assertEquals(HttpStatus.NO_CONTENT_204, jsonArray.getJSONObject(1).getInt("statusCode"));
|
assertEquals(HttpStatus.NO_CONTENT_204, jsonArray.getJSONObject(1).getInt("statusCode"));
|
||||||
|
assertEquals(2, jsonArray.getJSONObject(1).getInt("id"));
|
||||||
|
|
||||||
assertEquals(HttpStatus.BAD_REQUEST_400, jsonArray.getJSONObject(2).getInt("statusCode"));
|
assertEquals(HttpStatus.BAD_REQUEST_400, jsonArray.getJSONObject(2).getInt("statusCode"));
|
||||||
assertEquals("Error updating Person: Missing value in primary key field", jsonArray.getJSONObject(2).getString("error"));
|
assertEquals("Error updating Person: Missing value in primary key field", jsonArray.getJSONObject(2).getString("error"));
|
||||||
|
assertFalse(jsonArray.getJSONObject(2).has("id"));
|
||||||
|
|
||||||
|
assertEquals(HttpStatus.NOT_FOUND_404, jsonArray.getJSONObject(3).getInt("statusCode"));
|
||||||
|
assertEquals("Error updating Person: No record was found to update for Id = 256", jsonArray.getJSONObject(3).getString("error"));
|
||||||
|
assertEquals(256, jsonArray.getJSONObject(3).getInt("id"));
|
||||||
|
|
||||||
QRecord record = getRecord(TestUtils.TABLE_NAME_PERSON, 1);
|
QRecord record = getRecord(TestUtils.TABLE_NAME_PERSON, 1);
|
||||||
assertEquals("homer@simpson.com", record.getValueString("email"));
|
assertEquals("homer@simpson.com", record.getValueString("email"));
|
||||||
@ -818,7 +828,7 @@ class QJavalinApiHandlerTest extends BaseTest
|
|||||||
|
|
||||||
QueryInput queryInput = new QueryInput();
|
QueryInput queryInput = new QueryInput();
|
||||||
queryInput.setTableName(TestUtils.TABLE_NAME_PERSON);
|
queryInput.setTableName(TestUtils.TABLE_NAME_PERSON);
|
||||||
queryInput.setFilter(new QQueryFilter(new QFilterCriteria("email", QCriteriaOperator.EQUALS, "nobody@simpson.com")));
|
queryInput.setFilter(new QQueryFilter(new QFilterCriteria("email", QCriteriaOperator.IN, List.of("nobody@simpson.com", "256@simpson.com"))));
|
||||||
QueryOutput queryOutput = new QueryAction().execute(queryInput);
|
QueryOutput queryOutput = new QueryAction().execute(queryInput);
|
||||||
assertEquals(0, queryOutput.getRecords().size());
|
assertEquals(0, queryOutput.getRecords().size());
|
||||||
}
|
}
|
||||||
@ -885,16 +895,24 @@ class QJavalinApiHandlerTest extends BaseTest
|
|||||||
|
|
||||||
HttpResponse<String> response = Unirest.delete(BASE_URL + "/api/" + VERSION + "/person/bulk")
|
HttpResponse<String> response = Unirest.delete(BASE_URL + "/api/" + VERSION + "/person/bulk")
|
||||||
.body("""
|
.body("""
|
||||||
[ 1, 3, 5 ]
|
[ 1, 3, 5, 7 ]
|
||||||
""")
|
""")
|
||||||
.asString();
|
.asString();
|
||||||
assertEquals(HttpStatus.MULTI_STATUS_207, response.getStatus());
|
assertEquals(HttpStatus.MULTI_STATUS_207, response.getStatus());
|
||||||
JSONArray jsonArray = new JSONArray(response.getBody());
|
JSONArray jsonArray = new JSONArray(response.getBody());
|
||||||
assertEquals(3, jsonArray.length());
|
assertEquals(4, jsonArray.length());
|
||||||
|
|
||||||
assertEquals(HttpStatus.NO_CONTENT_204, jsonArray.getJSONObject(0).getInt("statusCode"));
|
assertEquals(HttpStatus.NO_CONTENT_204, jsonArray.getJSONObject(0).getInt("statusCode"));
|
||||||
|
assertEquals(1, jsonArray.getJSONObject(0).getInt("id"));
|
||||||
|
|
||||||
assertEquals(HttpStatus.NO_CONTENT_204, jsonArray.getJSONObject(1).getInt("statusCode"));
|
assertEquals(HttpStatus.NO_CONTENT_204, jsonArray.getJSONObject(1).getInt("statusCode"));
|
||||||
|
assertEquals(3, jsonArray.getJSONObject(1).getInt("id"));
|
||||||
|
|
||||||
assertEquals(HttpStatus.NO_CONTENT_204, jsonArray.getJSONObject(2).getInt("statusCode"));
|
assertEquals(HttpStatus.NO_CONTENT_204, jsonArray.getJSONObject(2).getInt("statusCode"));
|
||||||
|
assertEquals(5, jsonArray.getJSONObject(2).getInt("id"));
|
||||||
|
|
||||||
|
assertEquals(HttpStatus.NOT_FOUND_404, jsonArray.getJSONObject(3).getInt("statusCode"));
|
||||||
|
assertEquals(7, jsonArray.getJSONObject(3).getInt("id"));
|
||||||
|
|
||||||
QueryInput queryInput = new QueryInput();
|
QueryInput queryInput = new QueryInput();
|
||||||
queryInput.setTableName(TestUtils.TABLE_NAME_PERSON);
|
queryInput.setTableName(TestUtils.TABLE_NAME_PERSON);
|
||||||
|
Reference in New Issue
Block a user