diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java index be89714c..7fb757fa 100644 --- a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java @@ -611,38 +611,17 @@ public class QJavalinImplementation updateInput.setTableName(table); PermissionsHelper.checkTablePermissionThrowing(updateInput, TablePermissionSubType.EDIT); + QTableMetaData tableMetaData = qInstance.getTable(table); + + QJavalinAccessLogger.logStart("update", logPair("table", table), logPair("primaryKey", primaryKey)); List recordList = new ArrayList<>(); QRecord record = new QRecord(); record.setTableName(table); recordList.add(record); - Map map = context.bodyAsClass(Map.class); - for(Map.Entry entry : map.entrySet()) - { - String fieldName = ValueUtils.getValueAsString(entry.getKey()); - Object value = entry.getValue(); - - if(StringUtils.hasContent(String.valueOf(value))) - { - record.setValue(fieldName, (Serializable) value); - } - else if("".equals(value)) - { - /////////////////////////////////////////////////////////////////////////////////////////////////// - // if frontend sent us an empty string - put a null in the record's value map. // - // this could potentially be changed to be type-specific (e.g., store an empty-string for STRING // - // fields, but null for INTEGER, etc) - but, who really wants empty-string in database anyway? // - /////////////////////////////////////////////////////////////////////////////////////////////////// - record.setValue(fieldName, null); - } - } - - QTableMetaData tableMetaData = qInstance.getTable(table); record.setValue(tableMetaData.getPrimaryKeyField(), primaryKey); - - QJavalinAccessLogger.logStart("update", logPair("table", table), logPair("primaryKey", primaryKey)); - + setRecordValuesForInsertOrUpdate(context, tableMetaData, record); updateInput.setRecords(recordList); UpdateAction updateAction = new UpdateAction(); @@ -660,6 +639,80 @@ public class QJavalinImplementation + /******************************************************************************* + ** + *******************************************************************************/ + private static void setRecordValuesForInsertOrUpdate(Context context, QTableMetaData tableMetaData, QRecord record) throws IOException + { + String contentType = Objects.requireNonNullElse(context.header("content-type"), ""); + boolean isContentTypeJson = contentType.toLowerCase().contains("json"); + + try + { + ////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // if the caller said they've sent JSON, or if they didn't supply a content-type, then try to read the body // + // as JSON. if it throws, we'll continue by trying to read form params, but if it succeeds, we'll return. // + ////////////////////////////////////////////////////////////////////////////////////////////////////////////// + if(isContentTypeJson || !StringUtils.hasContent(contentType)) + { + Map map = context.bodyAsClass(Map.class); + for(Map.Entry entry : map.entrySet()) + { + String fieldName = ValueUtils.getValueAsString(entry.getKey()); + Object value = entry.getValue(); + + if(StringUtils.hasContent(String.valueOf(value))) + { + record.setValue(fieldName, (Serializable) value); + } + else if("".equals(value)) + { + /////////////////////////////////////////////////////////////////////////////////////////////////// + // if frontend sent us an empty string - put a null in the record's value map. // + // this could potentially be changed to be type-specific (e.g., store an empty-string for STRING // + // fields, but null for INTEGER, etc) - but, who really wants empty-string in database anyway? // + /////////////////////////////////////////////////////////////////////////////////////////////////// + record.setValue(fieldName, null); + } + } + + return; + } + } + catch(Exception e) + { + LOG.info("Error trying to read body as map", e, logPair("contentType", contentType)); + } + + ///////////////////////// + // process form params // + ///////////////////////// + for(Map.Entry> formParam : context.formParamMap().entrySet()) + { + String fieldName = formParam.getKey(); + List values = formParam.getValue(); + if(CollectionUtils.nullSafeHasContents(values)) + { + String value = values.get(0); + if(StringUtils.hasContent(value)) + { + record.setValue(fieldName, value); + } + else + { + record.setValue(fieldName, null); + } + } + else + { + // is this ever hit? + record.setValue(fieldName, null); + } + } + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -674,20 +727,13 @@ public class QJavalinImplementation QJavalinAccessLogger.logStart("insert", logPair("table", tableName)); PermissionsHelper.checkTablePermissionThrowing(insertInput, TablePermissionSubType.INSERT); + QTableMetaData tableMetaData = qInstance.getTable(tableName); List recordList = new ArrayList<>(); QRecord record = new QRecord(); record.setTableName(tableName); recordList.add(record); - - Map map = context.bodyAsClass(Map.class); - for(Map.Entry entry : map.entrySet()) - { - if(StringUtils.hasContent(String.valueOf(entry.getValue()))) - { - record.setValue(String.valueOf(entry.getKey()), (Serializable) entry.getValue()); - } - } + setRecordValuesForInsertOrUpdate(context, tableMetaData, record); insertInput.setRecords(recordList); InsertAction insertAction = new InsertAction(); @@ -695,14 +741,14 @@ public class QJavalinImplementation if(CollectionUtils.nullSafeHasContents(insertOutput.getRecords().get(0).getErrors())) { - throw (new QUserFacingException("Error inserting " + qInstance.getTable(tableName).getLabel() + ": " + insertOutput.getRecords().get(0).getErrors().get(0))); + throw (new QUserFacingException("Error inserting " + tableMetaData.getLabel() + ": " + insertOutput.getRecords().get(0).getErrors().get(0))); } if(CollectionUtils.nullSafeHasContents(insertOutput.getRecords().get(0).getWarnings())) { - throw (new QUserFacingException("Warning inserting " + qInstance.getTable(tableName).getLabel() + ": " + insertOutput.getRecords().get(0).getWarnings().get(0))); + throw (new QUserFacingException("Warning inserting " + tableMetaData.getLabel() + ": " + insertOutput.getRecords().get(0).getWarnings().get(0))); } - QJavalinAccessLogger.logEndSuccess(logPair("primaryKey", () -> (insertOutput.getRecords().get(0).getValue(qInstance.getTable(tableName).getPrimaryKeyField())))); + QJavalinAccessLogger.logEndSuccess(logPair("primaryKey", () -> (insertOutput.getRecords().get(0).getValue(tableMetaData.getPrimaryKeyField())))); context.result(JsonUtils.toJson(insertOutput)); } catch(Exception e) diff --git a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java index 0fa2e777..722cba48 100644 --- a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java +++ b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java @@ -391,11 +391,13 @@ class QJavalinImplementationTest extends QJavalinTestBase /******************************************************************************* - ** test an insert + ** test an insert - posting the data as a JSON object. + ** + ** This was the original supported version, but multipart form was added in May 2023 ** *******************************************************************************/ @Test - public void test_dataInsert() + public void test_dataInsertJson() { Map body = new HashMap<>(); body.put("firstName", "Bobby"); @@ -425,11 +427,45 @@ class QJavalinImplementationTest extends QJavalinTestBase /******************************************************************************* - ** test an update + ** test an insert - posting a multipart form. ** *******************************************************************************/ @Test - public void test_dataUpdate() + public void test_dataInsertMultipartForm() + { + HttpResponse response = Unirest.post(BASE_URL + "/data/person") + .header("Content-Type", "application/json") + .multiPartContent() + .field("firstName", "Bobby") + .field("lastName", "Hull") + .field("email", "bobby@hull.com") + .asString(); + + assertEquals(200, response.getStatus()); + JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); + assertTrue(jsonObject.has("records")); + JSONArray records = jsonObject.getJSONArray("records"); + assertEquals(1, records.length()); + JSONObject record0 = records.getJSONObject(0); + assertTrue(record0.has("values")); + assertEquals("person", record0.getString("tableName")); + JSONObject values0 = record0.getJSONObject("values"); + assertTrue(values0.has("firstName")); + assertEquals("Bobby", values0.getString("firstName")); + assertTrue(values0.has("id")); + assertEquals(7, values0.getInt("id")); + } + + + + /******************************************************************************* + ** test an update - posting the data as a JSON object. + ** + ** This was the original supported version, but multipart form was added in May 2023 + ** + *******************************************************************************/ + @Test + public void test_dataUpdateJson() { Map body = new HashMap<>(); body.put("firstName", "Free"); @@ -465,6 +501,44 @@ class QJavalinImplementationTest extends QJavalinTestBase + /******************************************************************************* + ** test an update - posting the data as a multipart form + ** + *******************************************************************************/ + @Test + public void test_dataUpdateMultipartForm() + { + HttpResponse response = Unirest.patch(BASE_URL + "/data/person/4") + .multiPartContent() + .field("firstName", "Free") + .field("birthDate", "") + .asString(); + + assertEquals(200, response.getStatus()); + JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); + assertTrue(jsonObject.has("records")); + JSONArray records = jsonObject.getJSONArray("records"); + assertEquals(1, records.length()); + JSONObject record0 = records.getJSONObject(0); + assertTrue(record0.has("values")); + assertEquals("person", record0.getString("tableName")); + JSONObject values0 = record0.getJSONObject("values"); + assertEquals(4, values0.getInt("id")); + assertEquals("Free", values0.getString("firstName")); + + /////////////////////////////////////////////////////////////////// + // re-GET the record, and validate that birthDate was nulled out // + /////////////////////////////////////////////////////////////////// + response = Unirest.get(BASE_URL + "/data/person/4").asString(); + assertEquals(200, response.getStatus()); + jsonObject = JsonUtils.toJSONObject(response.getBody()); + assertTrue(jsonObject.has("values")); + JSONObject values = jsonObject.getJSONObject("values"); + assertFalse(values.has("birthDate")); + } + + + /******************************************************************************* ** test a delete **