Bulk update & delete; errors if more than jsut the expected json

This commit is contained in:
2023-03-23 12:44:40 -05:00
parent 1d2acc7364
commit 74cf24a00e
5 changed files with 576 additions and 35 deletions

View File

@ -91,6 +91,7 @@ import org.apache.commons.lang.BooleanUtils;
import org.eclipse.jetty.http.HttpStatus;
import org.json.JSONArray;
import org.json.JSONObject;
import org.json.JSONTokener;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
import static com.kingsrook.qqq.backend.javalin.QJavalinImplementation.SLOW_LOG_THRESHOLD_MS;
@ -137,13 +138,16 @@ public class QJavalinApiHandler
ApiBuilder.get("/query", QJavalinApiHandler::doQuery);
// ApiBuilder.post("/query", QJavalinApiHandler::doQuery);
ApiBuilder.post("/bulk", QJavalinApiHandler::bulkInsert);
ApiBuilder.patch("/bulk", QJavalinApiHandler::bulkUpdate);
ApiBuilder.delete("/bulk", QJavalinApiHandler::bulkDelete);
//////////////////////////////////////////////////////////////////
// remember to keep the wildcard paths after the specific paths //
//////////////////////////////////////////////////////////////////
ApiBuilder.get("/{primaryKey}", QJavalinApiHandler::doGet);
ApiBuilder.patch("/{primaryKey}", QJavalinApiHandler::doUpdate);
ApiBuilder.delete("/{primaryKey}", QJavalinApiHandler::doDelete);
ApiBuilder.post("/bulk", QJavalinApiHandler::bulkInsert);
// patch("/bulk", QJavalinApiHandler::bulkUpdate);
// delete("/bulk", QJavalinApiHandler::bulkDelete);
});
});
@ -722,8 +726,15 @@ public class QJavalinApiHandler
throw (new QBadRequestException("Missing required POST body"));
}
JSONObject jsonObject = new JSONObject(context.body());
JSONTokener jsonTokener = new JSONTokener(context.body().trim());
JSONObject jsonObject = new JSONObject(jsonTokener);
insertInput.setRecords(List.of(QRecordApiAdapter.apiJsonObjectToQRecord(jsonObject, tableName, version)));
if(jsonTokener.more())
{
throw (new QBadRequestException("Body contained more than a single JSON object."));
}
}
catch(QBadRequestException qbre)
{
@ -787,13 +798,21 @@ public class QJavalinApiHandler
ArrayList<QRecord> recordList = new ArrayList<>();
insertInput.setRecords(recordList);
JSONArray jsonArray = new JSONArray(context.body());
JSONTokener jsonTokener = new JSONTokener(context.body().trim());
JSONArray jsonArray = new JSONArray(jsonTokener);
for(int i = 0; i < jsonArray.length(); i++)
{
JSONObject jsonObject = jsonArray.getJSONObject(i);
recordList.add(QRecordApiAdapter.apiJsonObjectToQRecord(jsonObject, tableName, version));
}
if(jsonTokener.more())
{
throw (new QBadRequestException("Body contained more than a single JSON array."));
}
if(recordList.isEmpty())
{
throw (new QBadRequestException("No records were found in the POST body"));
@ -851,6 +870,229 @@ public class QJavalinApiHandler
/*******************************************************************************
**
*******************************************************************************/
private static void bulkUpdate(Context context)
{
String version = context.pathParam("version");
String tableApiName = context.pathParam("tableName");
try
{
QTableMetaData table = validateTableAndVersion(context, version, tableApiName);
String tableName = table.getName();
UpdateInput updateInput = new UpdateInput();
setupSession(context, updateInput);
QJavalinAccessLogger.logStart("bulkUpdate", logPair("table", tableName));
updateInput.setTableName(tableName);
PermissionsHelper.checkTablePermissionThrowing(updateInput, TablePermissionSubType.EDIT);
/////////////////
// build input //
/////////////////
try
{
if(!StringUtils.hasContent(context.body()))
{
throw (new QBadRequestException("Missing required PATCH body"));
}
ArrayList<QRecord> recordList = new ArrayList<>();
updateInput.setRecords(recordList);
JSONTokener jsonTokener = new JSONTokener(context.body().trim());
JSONArray jsonArray = new JSONArray(jsonTokener);
for(int i = 0; i < jsonArray.length(); i++)
{
JSONObject jsonObject = jsonArray.getJSONObject(i);
recordList.add(QRecordApiAdapter.apiJsonObjectToQRecord(jsonObject, tableName, version));
}
if(jsonTokener.more())
{
throw (new QBadRequestException("Body contained more than a single JSON array."));
}
if(recordList.isEmpty())
{
throw (new QBadRequestException("No records were found in the PATCH body"));
}
}
catch(QBadRequestException qbre)
{
throw (qbre);
}
catch(Exception e)
{
throw (new QBadRequestException("Body could not be parsed as a JSON array: " + e.getMessage(), e));
}
//////////////
// execute! //
//////////////
UpdateAction updateAction = new UpdateAction();
UpdateOutput updateOutput = updateAction.execute(updateInput);
///////////////////////////////////////
// process records to build response //
///////////////////////////////////////
List<Map<String, Serializable>> response = new ArrayList<>();
for(QRecord record : updateOutput.getRecords())
{
LinkedHashMap<String, Serializable> outputRecord = new LinkedHashMap<>();
response.add(outputRecord);
List<String> errors = record.getErrors();
if(CollectionUtils.nullSafeHasContents(errors))
{
outputRecord.put("statusCode", HttpStatus.Code.BAD_REQUEST.getCode());
outputRecord.put("statusText", HttpStatus.Code.BAD_REQUEST.getMessage());
outputRecord.put("error", "Error updating " + table.getLabel() + ": " + StringUtils.joinWithCommasAndAnd(errors));
}
else
{
outputRecord.put("statusCode", HttpStatus.Code.NO_CONTENT.getCode());
outputRecord.put("statusText", HttpStatus.Code.NO_CONTENT.getMessage());
}
}
QJavalinAccessLogger.logEndSuccess();
context.status(HttpStatus.Code.MULTI_STATUS.getCode());
context.result(JsonUtils.toJson(response));
}
catch(Exception e)
{
QJavalinAccessLogger.logEndFail(e);
handleException(context, e);
}
}
/*******************************************************************************
**
*******************************************************************************/
private static void bulkDelete(Context context)
{
String version = context.pathParam("version");
String tableApiName = context.pathParam("tableName");
try
{
QTableMetaData table = validateTableAndVersion(context, version, tableApiName);
String tableName = table.getName();
DeleteInput deleteInput = new DeleteInput();
setupSession(context, deleteInput);
QJavalinAccessLogger.logStart("bulkDelete", logPair("table", tableName));
deleteInput.setTableName(tableName);
PermissionsHelper.checkTablePermissionThrowing(deleteInput, TablePermissionSubType.DELETE);
/////////////////
// build input //
/////////////////
try
{
if(!StringUtils.hasContent(context.body()))
{
throw (new QBadRequestException("Missing required DELETE body"));
}
ArrayList<Serializable> primaryKeyList = new ArrayList<>();
deleteInput.setPrimaryKeys(primaryKeyList);
JSONTokener jsonTokener = new JSONTokener(context.body().trim());
JSONArray jsonArray = new JSONArray(jsonTokener);
for(int i = 0; i < jsonArray.length(); i++)
{
Object object = jsonArray.get(i);
if(object instanceof JSONArray || object instanceof JSONObject)
{
throw (new QBadRequestException("One or more elements inside the DELETE body JSONArray was not a primitive value"));
}
primaryKeyList.add(String.valueOf(object));
}
if(jsonTokener.more())
{
throw (new QBadRequestException("Body contained more than a single JSON array."));
}
if(primaryKeyList.isEmpty())
{
throw (new QBadRequestException("No primary keys were found in the DELETE body"));
}
}
catch(QBadRequestException qbre)
{
throw (qbre);
}
catch(Exception e)
{
throw (new QBadRequestException("Body could not be parsed as a JSON array: " + e.getMessage(), e));
}
//////////////
// execute! //
//////////////
DeleteAction deleteAction = new DeleteAction();
DeleteOutput deleteOutput = deleteAction.execute(deleteInput);
///////////////////////////////////////
// process records to build response //
///////////////////////////////////////
List<Map<String, Serializable>> response = new ArrayList<>();
List<QRecord> recordsWithErrors = deleteOutput.getRecordsWithErrors();
Map<String, String> primaryKeyToErrorMap = new HashMap<>();
for(QRecord recordWithError : CollectionUtils.nonNullList(recordsWithErrors))
{
String primaryKey = recordWithError.getValueString(table.getPrimaryKeyField());
primaryKeyToErrorMap.put(primaryKey, StringUtils.join(", ", recordWithError.getErrors()));
}
for(Serializable primaryKey : deleteInput.getPrimaryKeys())
{
LinkedHashMap<String, Serializable> outputRecord = new LinkedHashMap<>();
response.add(outputRecord);
String primaryKeyString = ValueUtils.getValueAsString((primaryKey));
if(primaryKeyToErrorMap.containsKey(primaryKeyString))
{
outputRecord.put("statusCode", HttpStatus.Code.BAD_REQUEST.getCode());
outputRecord.put("statusText", HttpStatus.Code.BAD_REQUEST.getMessage());
outputRecord.put("error", "Error deleting " + table.getLabel() + ": " + primaryKeyToErrorMap.get(primaryKeyString));
}
else
{
outputRecord.put("statusCode", HttpStatus.Code.NO_CONTENT.getCode());
outputRecord.put("statusText", HttpStatus.Code.NO_CONTENT.getMessage());
}
}
QJavalinAccessLogger.logEndSuccess();
context.status(HttpStatus.Code.MULTI_STATUS.getCode());
context.result(JsonUtils.toJson(response));
}
catch(Exception e)
{
QJavalinAccessLogger.logEndFail(e);
handleException(context, e);
}
}
/*******************************************************************************
**
*******************************************************************************/
@ -892,13 +1134,20 @@ public class QJavalinApiHandler
{
if(!StringUtils.hasContent(context.body()))
{
throw (new QBadRequestException("Missing required POST body"));
throw (new QBadRequestException("Missing required PATCH body"));
}
JSONObject jsonObject = new JSONObject(context.body());
QRecord qRecord = QRecordApiAdapter.apiJsonObjectToQRecord(jsonObject, tableName, version);
JSONTokener jsonTokener = new JSONTokener(context.body().trim());
JSONObject jsonObject = new JSONObject(jsonTokener);
QRecord qRecord = QRecordApiAdapter.apiJsonObjectToQRecord(jsonObject, tableName, version);
qRecord.setValue(table.getPrimaryKeyField(), primaryKey);
updateInput.setRecords(List.of(qRecord));
if(jsonTokener.more())
{
throw (new QBadRequestException("Body contained more than a single JSON object."));
}
}
catch(QBadRequestException qbre)
{

View File

@ -29,11 +29,17 @@ import com.kingsrook.qqq.api.TestUtils;
import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaData;
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
@ -442,17 +448,16 @@ class QJavalinApiHandlerTest extends BaseTest
QRecord personRecord = getPersonRecord(1);
assertNull(personRecord);
///////////////////////////////////////////////////////////////////////////////////////////
// apparently, as long as the body *starts with* json, the JSONObject constructor builds //
// a json object out of it?? so... this in this case we expected 400, but get 201... //
///////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////
// If more than just a json object, fail //
///////////////////////////////////////////
response = Unirest.post(BASE_URL + "/api/" + VERSION + "/person/")
.body("""
{"firstName": "Moe"}
Not json
""")
.asString();
assertErrorResponse(HttpStatus.CREATED_201, null, response);
assertErrorResponse(HttpStatus.BAD_REQUEST_400, "Body contained more than a single JSON object", response);
}
@ -540,17 +545,16 @@ class QJavalinApiHandlerTest extends BaseTest
QRecord personRecord = getPersonRecord(1);
assertNull(personRecord);
///////////////////////////////////////////////////////////////////////////////////////////
// apparently, as long as the body *starts with* json, the JSONObject constructor builds //
// a json object out of it?? so... this in this case we expected 400, but get 201... //
///////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////
// If more than just a json array, fail //
//////////////////////////////////////////
response = Unirest.post(BASE_URL + "/api/" + VERSION + "/person/bulk")
.body("""
[{"firstName": "Moe"}]
Not json
""")
.asString();
assertErrorResponse(HttpStatus.MULTI_STATUS_207, null, response);
assertErrorResponse(HttpStatus.BAD_REQUEST_400, "Body contained more than a single JSON array", response);
}
@ -618,7 +622,7 @@ class QJavalinApiHandlerTest extends BaseTest
response = Unirest.patch(BASE_URL + "/api/" + VERSION + "/person/1")
// no body
.asString();
assertErrorResponse(HttpStatus.BAD_REQUEST_400, "Missing required POST body", response);
assertErrorResponse(HttpStatus.BAD_REQUEST_400, "Missing required PATCH body", response);
response = Unirest.patch(BASE_URL + "/api/" + VERSION + "/person/1")
.body("""
@ -647,17 +651,185 @@ class QJavalinApiHandlerTest extends BaseTest
QRecord personRecord = getPersonRecord(1);
assertEquals("Mo", personRecord.getValueString("firstName"));
///////////////////////////////////////////////////////////////////////////////////////////
// apparently, as long as the body *starts with* json, the JSONObject constructor builds //
// a json object out of it?? so... this in this case we expected 400, but get 204... //
///////////////////////////////////////////////////////////////////////////////////////////
response = Unirest.patch(BASE_URL + "/api/" + VERSION + "/person/1")
.body("""
{"firstName": "Moe"}
Not json
""")
.asString();
assertErrorResponse(HttpStatus.NO_CONTENT_204, null, response);
assertErrorResponse(HttpStatus.BAD_REQUEST_400, "Body contained more than a single JSON object", response);
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testBulkUpdate207() throws QException
{
insertSimpsons();
HttpResponse<String> response = Unirest.patch(BASE_URL + "/api/" + VERSION + "/person/bulk")
.body("""
[
{"id": 1, "email": "homer@simpson.com"},
{"id": 2, "email": "marge@simpson.com"},
{"email": "nobody@simpson.com"}
]
""")
.asString();
assertEquals(HttpStatus.MULTI_STATUS_207, response.getStatus());
JSONArray jsonArray = new JSONArray(response.getBody());
assertEquals(3, jsonArray.length());
assertEquals(HttpStatus.NO_CONTENT_204, jsonArray.getJSONObject(0).getInt("statusCode"));
assertEquals(HttpStatus.NO_CONTENT_204, jsonArray.getJSONObject(1).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"));
QRecord record = getPersonRecord(1);
assertEquals("homer@simpson.com", record.getValueString("email"));
record = getPersonRecord(2);
assertEquals("marge@simpson.com", record.getValueString("email"));
QueryInput queryInput = new QueryInput();
queryInput.setTableName(TestUtils.TABLE_NAME_PERSON);
queryInput.setFilter(new QQueryFilter(new QFilterCriteria("email", QCriteriaOperator.EQUALS, "nobody@simpson.com")));
QueryOutput queryOutput = new QueryAction().execute(queryInput);
assertEquals(0, queryOutput.getRecords().size());
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testBulkUpdate400s() throws QException
{
HttpResponse<String> response = Unirest.patch(BASE_URL + "/api/" + VERSION + "/person/bulk")
.body("""
{"firstName": "Moe"}
""")
.asString();
assertErrorResponse(HttpStatus.BAD_REQUEST_400, "Body could not be parsed as a JSON array: A JSONArray text must start with '['", response);
response = Unirest.patch(BASE_URL + "/api/" + VERSION + "/person/bulk")
// no body
.asString();
assertErrorResponse(HttpStatus.BAD_REQUEST_400, "Missing required PATCH body", response);
response = Unirest.patch(BASE_URL + "/api/" + VERSION + "/person/bulk")
.body("[]")
.asString();
assertErrorResponse(HttpStatus.BAD_REQUEST_400, "No records were found in the PATCH body", response);
response = Unirest.patch(BASE_URL + "/api/" + VERSION + "/person/bulk")
.body("""
[{"firstName": "Moe", "foo": "bar"}]
""")
.asString();
assertErrorResponse(HttpStatus.BAD_REQUEST_400, "Request body contained 1 unrecognized field name: foo", response);
////////////////////////////////
// assert nothing got updated //
////////////////////////////////
QRecord personRecord = getPersonRecord(1);
assertNull(personRecord);
//////////////////////////////////////////
// If more than just a json array, fail //
//////////////////////////////////////////
response = Unirest.patch(BASE_URL + "/api/" + VERSION + "/person/bulk")
.body("""
[{"firstName": "Moe"}]
Not json
""")
.asString();
assertErrorResponse(HttpStatus.BAD_REQUEST_400, "Body contained more than a single JSON array", response);
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testBulkDelete207() throws QException
{
insertSimpsons();
HttpResponse<String> response = Unirest.delete(BASE_URL + "/api/" + VERSION + "/person/bulk")
.body("""
[ 1, 3, 5 ]
""")
.asString();
assertEquals(HttpStatus.MULTI_STATUS_207, response.getStatus());
JSONArray jsonArray = new JSONArray(response.getBody());
assertEquals(3, jsonArray.length());
assertEquals(HttpStatus.NO_CONTENT_204, jsonArray.getJSONObject(0).getInt("statusCode"));
assertEquals(HttpStatus.NO_CONTENT_204, jsonArray.getJSONObject(1).getInt("statusCode"));
assertEquals(HttpStatus.NO_CONTENT_204, jsonArray.getJSONObject(2).getInt("statusCode"));
QueryInput queryInput = new QueryInput();
queryInput.setTableName(TestUtils.TABLE_NAME_PERSON);
QueryOutput queryOutput = new QueryAction().execute(queryInput);
assertEquals(2, queryOutput.getRecords().size());
assertEquals(List.of(2, 4), queryOutput.getRecords().stream().map(r -> r.getValueInteger("id")).toList());
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testBulkDelete400s() throws QException
{
HttpResponse<String> response = Unirest.delete(BASE_URL + "/api/" + VERSION + "/person/bulk")
.body("""
1, 2, 3
""")
.asString();
assertErrorResponse(HttpStatus.BAD_REQUEST_400, "Body could not be parsed as a JSON array: A JSONArray text must start with '['", response);
response = Unirest.delete(BASE_URL + "/api/" + VERSION + "/person/bulk")
// no body
.asString();
assertErrorResponse(HttpStatus.BAD_REQUEST_400, "Missing required DELETE body", response);
response = Unirest.delete(BASE_URL + "/api/" + VERSION + "/person/bulk")
.body("[]")
.asString();
assertErrorResponse(HttpStatus.BAD_REQUEST_400, "No primary keys were found in the DELETE body", response);
response = Unirest.delete(BASE_URL + "/api/" + VERSION + "/person/bulk")
.body("""
[{"id": 1}]
""")
.asString();
assertErrorResponse(HttpStatus.BAD_REQUEST_400, "One or more elements inside the DELETE body JSONArray was not a primitive value", response);
////////////////////////////////
// assert nothing got deleted //
////////////////////////////////
QRecord personRecord = getPersonRecord(1);
assertNull(personRecord);
//////////////////////////////////////////
// If more than just a json array, fail //
//////////////////////////////////////////
response = Unirest.delete(BASE_URL + "/api/" + VERSION + "/person/bulk")
.body("""
[1,2,3]
Not json
""")
.asString();
assertErrorResponse(HttpStatus.BAD_REQUEST_400, "Body contained more than a single JSON array", response);
}