From eaff7ccece02e32f3e7abea1c0ff617a248a5d72 Mon Sep 17 00:00:00 2001 From: Manfred Zingl Date: Mon, 23 Oct 2023 13:25:37 +0200 Subject: [PATCH 1/9] update open api doc version, fix some minor issues --- build.gradle | 2 +- src/main/resources/swagger.json | 31 +++++++++++-------- .../tableaux/router/DocumentationRouter.scala | 2 +- 3 files changed, 20 insertions(+), 15 deletions(-) diff --git a/build.gradle b/build.gradle index 44cda6ec..16f2b768 100755 --- a/build.gradle +++ b/build.gradle @@ -64,7 +64,7 @@ dependencies { implementation 'com.github.cb372:scalacache-guava_2.12:0.28.0' - implementation 'org.webjars:swagger-ui:4.11.1' + implementation 'org.webjars:swagger-ui:5.9.0' // Also update version in DocumentationRouter compileOnly "io.vertx:vertx-codegen:${vertxVersion}" diff --git a/src/main/resources/swagger.json b/src/main/resources/swagger.json index 91aaff37..66d30ca7 100644 --- a/src/main/resources/swagger.json +++ b/src/main/resources/swagger.json @@ -2318,19 +2318,10 @@ ] }, "historyType": { - "description": "The type of the history entry", - "type": "string", - "enum": [ - "row", - "cell", - "comment", - "cell_flag", - "row_flag", - "row_permissions" - ] + "$ref": "#/definitions/Enum: HistoryType" }, "languageType": { - "description": "Name of the user who has triggered a change", + "description": "The language type of the column", "type": "string", "enum": [ "neutral", @@ -3582,6 +3573,18 @@ "listener" ] }, + "Enum: HistoryType": { + "type": "string", + "description": "The type of the history entry", + "enum": [ + "row", + "cell", + "comment", + "cell_flag", + "row_flag", + "row_permissions" + ] + }, "Response: Subfolder": { "type": "object", "required": [ @@ -3847,8 +3850,10 @@ "description": "The type of the history to filter for.", "in": "query", "type": "string", - "format": "string", - "required": false + "required": false, + "items": { + "$ref": "#/definitions/Enum: HistoryType" + } }, "groupId": { "name": "groupId", diff --git a/src/main/scala/com/campudus/tableaux/router/DocumentationRouter.scala b/src/main/scala/com/campudus/tableaux/router/DocumentationRouter.scala index 36b6592d..841e6c68 100644 --- a/src/main/scala/com/campudus/tableaux/router/DocumentationRouter.scala +++ b/src/main/scala/com/campudus/tableaux/router/DocumentationRouter.scala @@ -19,7 +19,7 @@ object DocumentationRouter { class DocumentationRouter(override val config: TableauxConfig) extends BaseRouter { - private val swaggerUiVersion = "4.11.1" + private val swaggerUiVersion = "5.9.0" private val directory = """(?[A-Za-z0-9-_\\.]{1,60}){1}""" private val file = s"""(?[A-Za-z0-9-_\\.]{1,60}){1}""" From 074719b424310dbe72dc79556780e718e3de0c04 Mon Sep 17 00:00:00 2001 From: Manfred Zingl Date: Mon, 23 Oct 2023 14:42:52 +0200 Subject: [PATCH 2/9] add several change value detection tests --- .../CreateHistoryChangeDetectionTest.scala | 1642 +++++++++++++++++ 1 file changed, 1642 insertions(+) create mode 100644 src/test/scala/com/campudus/tableaux/api/content/CreateHistoryChangeDetectionTest.scala diff --git a/src/test/scala/com/campudus/tableaux/api/content/CreateHistoryChangeDetectionTest.scala b/src/test/scala/com/campudus/tableaux/api/content/CreateHistoryChangeDetectionTest.scala new file mode 100644 index 00000000..4916c7b6 --- /dev/null +++ b/src/test/scala/com/campudus/tableaux/api/content/CreateHistoryChangeDetectionTest.scala @@ -0,0 +1,1642 @@ +package com.campudus.tableaux.api.content + +import com.campudus.tableaux.api.media.MediaTestBase +import com.campudus.tableaux.database.DatabaseConnection +import com.campudus.tableaux.testtools.RequestCreation.{CurrencyCol, MultiCountry, Rows} +import com.campudus.tableaux.testtools.TableauxTestBase + +import io.vertx.core.json.JsonArray +import io.vertx.ext.unit.TestContext +import io.vertx.ext.unit.junit.VertxUnitRunner +import io.vertx.scala.SQLConnection +import org.vertx.scala.core.json.{Json, JsonObject} + +import scala.concurrent.Future + +import org.junit.{Ignore, Test} +import org.junit.Assert._ +import org.junit.runner.RunWith +import org.skyscreamer.jsonassert.{JSONAssert, JSONCompareMode} + +// trait TestHelper2 extends MediaTestBase { + +// def toRowsArray(obj: JsonObject): JsonArray = { +// obj.getJsonArray("rows") +// } + +// def getLinksValue(arr: JsonArray, pos: Int): JsonArray = { +// arr.getJsonObject(pos).getJsonArray("value") +// } + +// protected def createTestAttachment(name: String)(implicit c: TestContext): Future[String] = { + +// val path = s"/com/campudus/tableaux/uploads/Screen.Shot.png" +// val mimetype = "application/png" + +// val file = Json.obj("title" -> Json.obj("de-DE" -> name)) + +// for { +// fileUuid <- createFile("de-DE", path, mimetype, None) map (_.getString("uuid")) +// _ <- sendRequest("PUT", s"/files/$fileUuid", file) + +// } yield fileUuid +// } +// } + +@RunWith(classOf[VertxUnitRunner]) +class CreateHistoryChangeDetectionTest extends TableauxTestBase with TestHelper { + + @Test + def changeSimpleValueTwice_changeACellOnlyOnce(implicit c: TestContext): Unit = { + okTest { + val newValue = Json.obj("value" -> "a fix value") + + for { + _ <- createEmptyDefaultTable() + _ <- sendRequest("POST", "/tables/1/rows") + _ <- sendRequest("POST", "/tables/1/columns/1/rows/1", newValue) + _ <- sendRequest("POST", "/tables/1/columns/1/rows/1", newValue) + rows <- sendRequest("GET", "/tables/1/columns/1/rows/1/history?historyType=cell").map(toRowsArray) + } yield { + println(s"rows: $rows") + assertEquals(1, rows.size()) + } + } + } + + @Test + def changeMultilanguageValueTwice_text(implicit c: TestContext): Unit = { + okTest { + val newValue = Json.obj("value" -> Json.obj("de-DE" -> "a fix value")) + + for { + _ <- createTableWithMultilanguageColumns("history test") + _ <- sendRequest("POST", "/tables/1/rows") + _ <- sendRequest("POST", "/tables/1/columns/1/rows/1", newValue) + _ <- sendRequest("POST", "/tables/1/columns/1/rows/1", newValue) + rows <- sendRequest("GET", "/tables/1/columns/1/rows/1/history?historyType=cell").map(toRowsArray) + } yield { + assertEquals(1, rows.size()) + } + } + } + + @Test + def changeMultilanguageValueTwice_boolean(implicit c: TestContext): Unit = { + okTest { + val newValue = Json.obj("value" -> Json.obj("de-DE" -> true)) + + for { + _ <- createTableWithMultilanguageColumns("history test") + _ <- sendRequest("POST", "/tables/1/rows") + _ <- + sendRequest( + "POST", + "/tables/1/columns/2/rows/1", + newValue + ) // Booleans always gets a initial history entry on first change, so +1 history row + _ <- sendRequest("POST", "/tables/1/columns/2/rows/1", newValue) + rows <- sendRequest("GET", "/tables/1/columns/1/rows/1/history?historyType=cell").map(toRowsArray) + } yield { + assertEquals(2, rows.size()) + } + } + } + + @Test + def changeMultilanguageValueTwice_numeric(implicit c: TestContext): Unit = { + okTest { + val newValue = Json.obj("value" -> Json.obj("de-DE" -> 42)) + + for { + _ <- createTableWithMultilanguageColumns("history test") + _ <- sendRequest("POST", "/tables/1/rows") + _ <- sendRequest("POST", "/tables/1/columns/3/rows/1", newValue) + _ <- sendRequest("POST", "/tables/1/columns/3/rows/1", newValue) + rows <- sendRequest("GET", "/tables/1/columns/1/rows/1/history?historyType=cell").map(toRowsArray) + } yield { + assertEquals(1, rows.size()) + } + } + } + + @Test + def changeMultilanguageValueTwice_datetime(implicit c: TestContext): Unit = { + okTest { + val newValue = Json.obj("value" -> Json.obj("de-DE" -> "2019-01-18T00:00:00.000Z")) + + for { + _ <- createTableWithMultilanguageColumns("history test") + _ <- sendRequest("POST", "/tables/1/rows") + _ <- sendRequest("POST", "/tables/1/columns/3/rows/1", newValue) + _ <- sendRequest("POST", "/tables/1/columns/3/rows/1", newValue) + rows <- sendRequest("GET", "/tables/1/columns/1/rows/1/history?historyType=cell").map(toRowsArray) + } yield { + assertEquals(1, rows.size()) + } + } + } + + @Test + def changeMultilanguageValueTwice_currency(implicit c: TestContext): Unit = { + okTest { + val newValue = Json.obj("value" -> Json.obj("DE" -> 2999.99)) + val multiCountryCurrencyColumn = MultiCountry(CurrencyCol("currency-column"), Seq("DE", "GB")) + + for { + _ <- createSimpleTableWithCell("table1", multiCountryCurrencyColumn) + _ <- sendRequest("POST", "/tables/1/rows") + _ <- sendRequest("POST", "/tables/1/columns/3/rows/1", newValue) + _ <- sendRequest("POST", "/tables/1/columns/3/rows/1", newValue) + rows <- sendRequest("GET", "/tables/1/columns/1/rows/1/history?historyType=cell").map(toRowsArray) + } yield { + assertEquals(2, rows.size()) + } + } + } +} + +// @RunWith(classOf[VertxUnitRunner]) +// class CreateHistoryChangeLinkDetectionTest extends LinkTestBase with TestHelper { + +// @Test +// def changeLinkValueTwice_xxx(implicit c: TestContext): Unit = { +// okTest { +// val twoLinks = Json.obj("value" -> Json.obj("values" -> Json.arr(1, 2))) + +// for { +// linkColumnId <- setupTwoTablesWithEmptyLinks() + +// _ <- sendRequest("PUT", s"/tables/1/columns/$linkColumnId/rows/1", twoLinks) +// rows <- sendRequest("GET", "/tables/1/columns/3/rows/1/history?historyType=cell").map(toRowsArray) +// } yield { +// assertEquals(2, rows.size()) +// } +// } +// } + +// @Test +// def changeLink_singleLanguageMultiIdentifiers(implicit c: TestContext): Unit = { +// okTest { +// val twoLinks = Json.obj("value" -> Json.obj("values" -> Json.arr(1, 2))) + +// for { +// linkColumnId <- setupTwoTablesWithEmptyLinks() + +// // Change target table structure to have a second identifier +// _ <- sendRequest("POST", s"/tables/2/columns/2", Json.obj("identifier" -> true)) + +// _ <- sendRequest("PUT", s"/tables/1/columns/$linkColumnId/rows/1", twoLinks) +// rows <- sendRequest("GET", "/tables/1/columns/3/rows/1/history?historyType=cell").map(toRowsArray) +// } yield { +// assertEquals(2, rows.size()) +// } +// } +// } +// } + +// @RunWith(classOf[VertxUnitRunner]) +// class CreateBidirectionalLinkHistoryTest extends LinkTestBase with TestHelper { + +// @Test +// def changeLink_addOneLink(implicit c: TestContext): Unit = { +// okTest { +// val putLink = Json.obj("value" -> Json.obj("values" -> Json.arr(5))) + +// val linkTable = """[ {"id": 5, "value": "table2RowId3"} ]""".stripMargin +// val targetLinkTable = """[ {"id": 1, "value": "table1row1"} ]""".stripMargin + +// for { +// _ <- setupTwoTablesWithEmptyLinks() + +// _ <- sendRequest("PUT", s"/tables/1/columns/3/rows/1", putLink) + +// history <- sendRequest("GET", "/tables/1/columns/3/rows/1/history?historyType=cell").map(toRowsArray) +// targetHistory <- sendRequest("GET", "/tables/2/columns/3/rows/5/history?historyType=cell").map(toRowsArray) +// historyLinks = getLinksValue(history, 0) +// historyTargetLinks = getLinksValue(targetHistory, 0) +// } yield { +// JSONAssert.assertEquals(linkTable, historyLinks.toString, JSONCompareMode.LENIENT) +// JSONAssert.assertEquals(targetLinkTable, historyTargetLinks.toString, JSONCompareMode.LENIENT) +// } +// } +// } + +// @Test +// def changeLink_addTwoLinksAtOnce(implicit c: TestContext): Unit = { +// okTest { +// val putLink = Json.obj("value" -> Json.obj("values" -> Json.arr(4, 5))) + +// val linkTable = """[ {"id": 4, "value": "table2RowId2"}, {"id": 5, "value": "table2RowId3"} ]""".stripMargin +// val targetLinkTable1 = """[ {"id": 1, "value": "table1row1"} ]""".stripMargin +// val targetLinkTable2 = """[ {"id": 1, "value": "table1row1"} ]""".stripMargin + +// for { +// _ <- setupTwoTablesWithEmptyLinks() + +// _ <- sendRequest("PUT", s"/tables/1/columns/3/rows/1", putLink) + +// history <- sendRequest("GET", "/tables/1/columns/3/rows/1/history?historyType=cell").map(toRowsArray) +// targetHistory1 <- sendRequest("GET", "/tables/2/columns/3/rows/4/history?historyType=cell").map(toRowsArray) +// targetHistory2 <- sendRequest("GET", "/tables/2/columns/3/rows/5/history?historyType=cell").map(toRowsArray) +// historyLinks = getLinksValue(history, 0) +// historyTargetLinks1 = getLinksValue(targetHistory1, 0) +// historyTargetLinks2 = getLinksValue(targetHistory2, 0) +// } yield { +// JSONAssert.assertEquals(linkTable, historyLinks.toString, JSONCompareMode.STRICT) +// JSONAssert.assertEquals(targetLinkTable1, historyTargetLinks1.toString, JSONCompareMode.LENIENT) +// JSONAssert.assertEquals(targetLinkTable2, historyTargetLinks2.toString, JSONCompareMode.LENIENT) +// } +// } +// } + +// @Test +// def changeLink_addASecondLink(implicit c: TestContext): Unit = { +// okTest { +// val putInitialLink = Json.obj("value" -> Json.obj("values" -> Json.arr(5))) +// val patchSecondLink = Json.obj("value" -> Json.obj("values" -> Json.arr(4))) + +// val linkTable = +// """[ +// | {"id": 5, "value": "table2RowId3"}, +// | {"id": 4, "value": "table2RowId2"} +// |]""".stripMargin + +// val backLink = """[ {"id": 1, "value": "table1row1"} ]""".stripMargin + +// for { +// _ <- setupTwoTablesWithEmptyLinks() + +// _ <- sendRequest("PUT", s"/tables/1/columns/3/rows/1", putInitialLink) +// _ <- sendRequest("PATCH", s"/tables/1/columns/3/rows/1", patchSecondLink) + +// history <- sendRequest("GET", "/tables/1/columns/3/rows/1/history?historyType=cell").map(toRowsArray) +// targetHistory1 <- sendRequest("GET", "/tables/2/columns/3/rows/4/history?historyType=cell").map(toRowsArray) +// targetHistory2 <- sendRequest("GET", "/tables/2/columns/3/rows/5/history?historyType=cell").map(toRowsArray) + +// historyLinks = getLinksValue(history, 1) +// historyTargetLinks1 = getLinksValue(targetHistory1, 0) +// historyTargetLinks2 = getLinksValue(targetHistory2, 0) +// } yield { +// JSONAssert.assertEquals(linkTable, historyLinks.toString, JSONCompareMode.STRICT) +// JSONAssert.assertEquals(backLink, historyTargetLinks1.toString, JSONCompareMode.LENIENT) +// assertEquals(1, targetHistory1.size()) +// JSONAssert.assertEquals(backLink, historyTargetLinks2.toString, JSONCompareMode.LENIENT) +// assertEquals(1, targetHistory2.size()) +// } +// } +// } + +// @Test +// def changeLink_oneLinkExisting_addTwoLinksAtOnce(implicit c: TestContext): Unit = { +// okTest { +// val putInitialLink = Json.obj("value" -> Json.obj("values" -> Json.arr(5))) +// val patchSecondLinks = Json.obj("value" -> Json.obj("values" -> Json.arr(4, 3))) + +// val linkTable = +// """[ +// | {"id": 5, "value": "table2RowId3"}, +// | {"id": 4, "value": "table2RowId2"}, +// | {"id": 3, "value": "table2RowId1"} +// |]""".stripMargin + +// val backLink = """[ {"id": 1, "value": "table1row1"} ]""".stripMargin + +// for { +// _ <- setupTwoTablesWithEmptyLinks() + +// _ <- sendRequest("PUT", s"/tables/1/columns/3/rows/1", putInitialLink) +// _ <- sendRequest("PATCH", s"/tables/1/columns/3/rows/1", patchSecondLinks) + +// history <- sendRequest("GET", "/tables/1/columns/3/rows/1/history?historyType=cell").map(toRowsArray) +// targetHistoryRows1 <- sendRequest("GET", "/tables/2/columns/3/rows/3/history?historyType=cell").map(toRowsArray) +// targetHistoryRows2 <- sendRequest("GET", "/tables/2/columns/3/rows/4/history?historyType=cell").map(toRowsArray) +// targetHistoryRows3 <- sendRequest("GET", "/tables/2/columns/3/rows/5/history?historyType=cell").map(toRowsArray) + +// initialHistoryLinks = history.getJsonObject(0).getJsonArray("value") +// historyLinks = history.getJsonObject(1).getJsonArray("value") +// historyTargetLinks1 = targetHistoryRows1.getJsonObject(0).getJsonArray("value") +// historyTargetLinks2 = targetHistoryRows2.getJsonObject(0).getJsonArray("value") +// historyTargetLinks3 = targetHistoryRows3.getJsonObject(0).getJsonArray("value") +// } yield { +// assertEquals(1, initialHistoryLinks.size()) +// assertEquals(3, historyLinks.size()) +// JSONAssert.assertEquals(linkTable, historyLinks.toString, JSONCompareMode.STRICT) + +// assertEquals(1, targetHistoryRows1.size()) +// assertEquals(1, targetHistoryRows2.size()) +// assertEquals(1, targetHistoryRows3.size()) + +// JSONAssert.assertEquals(backLink, historyTargetLinks1.toString, JSONCompareMode.LENIENT) +// JSONAssert.assertEquals(backLink, historyTargetLinks2.toString, JSONCompareMode.LENIENT) +// JSONAssert.assertEquals(backLink, historyTargetLinks3.toString, JSONCompareMode.LENIENT) +// } +// } +// } + +// @Test +// def changeLink_reverseOrder_doesNotChangeBackLinks(implicit c: TestContext): Unit = { +// okTest { +// val putLinks = +// """ +// |{"value": +// | { "values": [3, 4, 5] } +// |} +// |""".stripMargin + +// val expected = +// """ +// |[ +// | {"id": 5, "value": "table2RowId3"}, +// | {"id": 4, "value": "table2RowId2"}, +// | {"id": 3, "value": "table2RowId1"} +// |] +// """.stripMargin + +// for { +// _ <- setupTwoTablesWithEmptyLinks() + +// _ <- sendRequest("PUT", s"/tables/1/columns/3/rows/1", Json.fromObjectString(putLinks)) + +// _ <- sendRequest("PUT", s"/tables/1/columns/3/rows/1/link/3/order", Json.obj("location" -> "end")) +// _ <- sendRequest("PUT", s"/tables/1/columns/3/rows/1/link/5/order", Json.obj("location" -> "start")) + +// backLinkHistory <- sendRequest("GET", "/tables/2/columns/3/rows/1/history?historyType=cell").map(toRowsArray) + +// rows <- sendRequest("GET", s"/tables/1/columns/3/rows/1/history?historyType=cell").map(toRowsArray) +// historyAfterCreation = getLinksValue(rows, 2) +// } yield { +// JSONAssert.assertEquals(expected, historyAfterCreation.toString, JSONCompareMode.STRICT_ORDER) +// assertEquals(0, backLinkHistory.size()) +// } +// } +// } + +// @Test +// def changeLink_threeExistingLinks_deleteOneLink(implicit c: TestContext): Unit = { +// okTest { +// val putLinks = Json.obj("value" -> Json.obj("values" -> Json.arr(3, 4, 5))) + +// val expected = +// """ +// |[ +// | {"id": 3, "value": "table2RowId1"}, +// | {"id": 5, "value": "table2RowId3"} +// |] +// """.stripMargin + +// for { +// _ <- setupTwoTablesWithEmptyLinks() + +// _ <- sendRequest("PUT", s"/tables/1/columns/3/rows/1", putLinks) +// _ <- sendRequest("DELETE", s"/tables/1/columns/3/rows/1/link/4") + +// rows <- sendRequest("GET", "/tables/1/columns/3/rows/1/history?historyType=cell").map(toRowsArray) +// historyAfterCreation = getLinksValue(rows, 1) +// backLinkRow3 <- sendRequest("GET", "/tables/2/columns/3/rows/3/history?historyType=cell").map(toRowsArray) +// backLinkRow4 <- sendRequest("GET", "/tables/2/columns/3/rows/4/history?historyType=cell").map(toRowsArray) +// backLinkRow5 <- sendRequest("GET", "/tables/2/columns/3/rows/5/history?historyType=cell").map(toRowsArray) + +// } yield { +// JSONAssert.assertEquals(expected, historyAfterCreation.toString, JSONCompareMode.STRICT_ORDER) + +// assertEquals(1, backLinkRow3.size()) +// assertEquals(2, backLinkRow4.size()) +// assertEquals(1, backLinkRow5.size()) + +// JSONAssert.assertEquals("""[{"id": 1}]""", getLinksValue(backLinkRow4, 0).toString, JSONCompareMode.LENIENT) +// JSONAssert.assertEquals("[]", getLinksValue(backLinkRow4, 1).toString, JSONCompareMode.LENIENT) +// } +// } +// } + +// @Test +// def changeLink_threeExistingLinks_deleteCell(implicit c: TestContext): Unit = { +// okTest { +// val putLinks = Json.obj("value" -> Json.obj("values" -> Json.arr(3, 4, 5))) + +// for { +// _ <- setupTwoTablesWithEmptyLinks() + +// _ <- sendRequest("PUT", s"/tables/1/columns/3/rows/1", putLinks) +// _ <- sendRequest("DELETE", s"/tables/1/columns/3/rows/1") + +// rows <- sendRequest("GET", "/tables/1/columns/3/rows/1/history?historyType=cell").map(toRowsArray) +// historyAfterCreation = getLinksValue(rows, 1) +// backLinkRow3 <- sendRequest("GET", "/tables/2/columns/3/rows/3/history?historyType=cell").map(toRowsArray) +// backLinkRow4 <- sendRequest("GET", "/tables/2/columns/3/rows/4/history?historyType=cell").map(toRowsArray) +// backLinkRow5 <- sendRequest("GET", "/tables/2/columns/3/rows/5/history?historyType=cell").map(toRowsArray) + +// } yield { +// JSONAssert.assertEquals("[]", historyAfterCreation.toString, JSONCompareMode.LENIENT) + +// assertEquals(2, backLinkRow3.size()) +// assertEquals(2, backLinkRow4.size()) +// assertEquals(2, backLinkRow5.size()) + +// JSONAssert.assertEquals("[]", getLinksValue(backLinkRow3, 1).toString, JSONCompareMode.LENIENT) +// JSONAssert.assertEquals("[]", getLinksValue(backLinkRow4, 1).toString, JSONCompareMode.LENIENT) +// JSONAssert.assertEquals("[]", getLinksValue(backLinkRow5, 1).toString, JSONCompareMode.LENIENT) +// } +// } +// } + +// @Test +// def changeLink_threeExistingLinks_putOne(implicit c: TestContext): Unit = { +// okTest { +// val putLinks = Json.obj("value" -> Json.obj("values" -> Json.arr(3, 4, 5))) +// val putNewLink = Json.obj("value" -> Json.obj("values" -> Json.arr(4))) + +// for { +// _ <- setupTwoTablesWithEmptyLinks() + +// _ <- sendRequest("PUT", s"/tables/1/columns/3/rows/1", putLinks) +// _ <- sendRequest("PUT", s"/tables/1/columns/3/rows/1", putNewLink) + +// rows <- sendRequest("GET", "/tables/1/columns/3/rows/1/history?historyType=cell").map(toRowsArray) +// historyAfterCreation = getLinksValue(rows, 1) +// backLinkRow3 <- sendRequest("GET", "/tables/2/columns/3/rows/3/history?historyType=cell").map(toRowsArray) +// backLinkRow4 <- sendRequest("GET", "/tables/2/columns/3/rows/4/history?historyType=cell").map(toRowsArray) +// backLinkRow5 <- sendRequest("GET", "/tables/2/columns/3/rows/5/history?historyType=cell").map(toRowsArray) + +// } yield { +// JSONAssert +// .assertEquals( +// """[{"id": 4, "value": "table2RowId2"}]""", +// historyAfterCreation.toString, +// JSONCompareMode.STRICT +// ) + +// assertEquals(2, backLinkRow3.size()) +// assertEquals(2, backLinkRow4.size()) +// assertEquals(2, backLinkRow5.size()) + +// JSONAssert.assertEquals("[]", getLinksValue(backLinkRow3, 1).toString, JSONCompareMode.LENIENT) +// JSONAssert.assertEquals( +// """[{"id": 1, "value": "table1row1"}]""", +// getLinksValue(backLinkRow4, 1).toString, +// JSONCompareMode.STRICT +// ) +// JSONAssert.assertEquals("[]", getLinksValue(backLinkRow5, 1).toString, JSONCompareMode.LENIENT) +// } +// } +// } + +// @Test +// def changeLink_twoLinksExistingInForeignTable_addAThirdLinkViaFirstTable(implicit c: TestContext): Unit = { +// okTest { +// val putInitialLinksFromTable1 = Json.obj("value" -> Json.obj("values" -> Json.arr(3, 4))) +// val patchThirdLinkFromTable2 = Json.obj("value" -> Json.obj("values" -> Json.arr(1))) + +// for { +// _ <- setupTwoTablesWithEmptyLinks() + +// _ <- sendRequest("PUT", s"/tables/2/columns/3/rows/1", putInitialLinksFromTable1) +// _ <- sendRequest("PATCH", s"/tables/1/columns/3/rows/5", patchThirdLinkFromTable2) + +// linksTable2Rows <- sendRequest("GET", "/tables/2/columns/3/rows/1/history?historyType=cell").map(toRowsArray) + +// linksTable1Rows3 <- sendRequest("GET", "/tables/1/columns/3/rows/3/history?historyType=cell").map(toRowsArray) +// linksTable1Rows4 <- sendRequest("GET", "/tables/1/columns/3/rows/4/history?historyType=cell").map(toRowsArray) +// linksTable1Rows5 <- sendRequest("GET", "/tables/1/columns/3/rows/5/history?historyType=cell").map(toRowsArray) + +// } yield { +// assertEquals(1, linksTable1Rows3.size()) +// assertEquals(1, linksTable1Rows4.size()) +// assertEquals(1, linksTable1Rows5.size()) + +// JSONAssert +// .assertEquals( +// """[{"id": 3}, {"id": 4}]""", +// getLinksValue(linksTable2Rows, 0).toString, +// JSONCompareMode.STRICT_ORDER +// ) +// JSONAssert +// .assertEquals( +// """[{"id": 3}, {"id": 4}, {"id": 5}]""", +// getLinksValue(linksTable2Rows, 1).toString, +// JSONCompareMode.STRICT_ORDER +// ) +// } +// } +// } + +// @Test +// def changeLink_addThreeLinksFromAndToTable(implicit c: TestContext): Unit = { +// okTest { +// val putInitialLinksFromTable1 = Json.obj("value" -> Json.obj("values" -> Json.arr(3, 4, 5))) +// val patchThirdLinkFromTable2 = Json.obj("value" -> Json.obj("values" -> Json.arr(3, 4, 5))) + +// val expectedLinksT1R3 = +// """[ +// | {"id": 4}, +// | {"id": 5}, +// | {"id": 3} +// |]""".stripMargin + +// val expectedLinksT2R3 = +// """[ +// | {"id": 3}, +// | {"id": 4}, +// | {"id": 5} +// |]""".stripMargin + +// val linkToTable2Row3 = """[{"id":3,"value":"table2RowId1"}]""" + +// val linkToTable1Row3 = """[{"id":3,"value":"table1RowId1"}]""" + +// for { +// _ <- setupTwoTablesWithEmptyLinks() + +// _ <- sendRequest("PUT", s"/tables/1/columns/3/rows/3", putInitialLinksFromTable1) +// _ <- sendRequest("PUT", s"/tables/2/columns/3/rows/3", patchThirdLinkFromTable2) + +// linksTable1Rows3 <- sendRequest("GET", "/tables/1/columns/3/rows/3/history?historyType=cell").map(toRowsArray) +// linksTable1Rows4 <- sendRequest("GET", "/tables/1/columns/3/rows/4/history?historyType=cell").map(toRowsArray) +// linksTable1Rows5 <- sendRequest("GET", "/tables/1/columns/3/rows/5/history?historyType=cell").map(toRowsArray) + +// linksTable2Rows3 <- sendRequest("GET", "/tables/2/columns/3/rows/3/history?historyType=cell").map(toRowsArray) +// linksTable2Rows4 <- sendRequest("GET", "/tables/2/columns/3/rows/4/history?historyType=cell").map(toRowsArray) +// linksTable2Rows5 <- sendRequest("GET", "/tables/2/columns/3/rows/5/history?historyType=cell").map(toRowsArray) + +// } yield { +// assertEquals(2, linksTable1Rows3.size()) +// assertEquals(1, linksTable1Rows4.size()) +// assertEquals(1, linksTable1Rows5.size()) + +// assertEquals(2, linksTable2Rows3.size()) +// assertEquals(1, linksTable2Rows4.size()) +// assertEquals(1, linksTable2Rows5.size()) + +// JSONAssert +// .assertEquals(expectedLinksT1R3, getLinksValue(linksTable1Rows3, 1).toString, JSONCompareMode.STRICT_ORDER) +// JSONAssert.assertEquals(linkToTable2Row3, getLinksValue(linksTable1Rows4, 0).toString, JSONCompareMode.LENIENT) +// JSONAssert.assertEquals(linkToTable2Row3, getLinksValue(linksTable1Rows5, 0).toString, JSONCompareMode.LENIENT) + +// JSONAssert +// .assertEquals(expectedLinksT2R3, getLinksValue(linksTable2Rows3, 1).toString, JSONCompareMode.STRICT_ORDER) +// JSONAssert.assertEquals(linkToTable1Row3, getLinksValue(linksTable2Rows4, 0).toString, JSONCompareMode.LENIENT) +// JSONAssert.assertEquals(linkToTable1Row3, getLinksValue(linksTable2Rows5, 0).toString, JSONCompareMode.LENIENT) +// } +// } +// } +// } + +// @RunWith(classOf[VertxUnitRunner]) +// class CreateMultiLanguageLinkHistoryTest extends LinkTestBase with TestHelper { + +// @Test +// def changeLink_MultiIdentifiers_MultiLangAndSingleLangNumeric(implicit c: TestContext): Unit = { +// okTest { + +// val expected = +// """ +// |[ +// | {"id":1,"value":{"de-DE":"Hallo, Table 2 Welt! 3.1415926","en-GB":"Hello, Table 2 World! 3.1415926"}}, +// | {"id":2,"value":{"de-DE":"Hallo, Table 2 Welt2! 2.1415926","en-GB":"Hello, Table 2 World2! 2.1415926"}} +// |] +// |""".stripMargin + +// val postLinkColumn = Json.obj( +// "columns" -> Json.arr( +// Json.obj( +// "name" -> "Test Link 1", +// "kind" -> "link", +// "toTable" -> 2 +// ) +// ) +// ) +// val putLinkValue = Json.obj("value" -> Json.obj("values" -> Json.arr(1, 2))) + +// for { +// _ <- createFullTableWithMultilanguageColumns("Table 1") +// _ <- createFullTableWithMultilanguageColumns("Table 2") + +// // Change target table structure to have a second identifier +// _ <- sendRequest("POST", s"/tables/2/columns/3", Json.obj("identifier" -> true)) + +// // Add link column +// linkColumn <- sendRequest("POST", s"/tables/1/columns", postLinkColumn) +// linkColumnId = linkColumn.getJsonArray("columns").get[JsonObject](0).getNumber("id") + +// _ <- sendRequest("PUT", s"/tables/1/columns/$linkColumnId/rows/1", putLinkValue) +// rows <- sendRequest("GET", "/tables/1/columns/8/rows/1/history?historyType=cell").map(toRowsArray) +// historyAfterCreation = getLinksValue(rows, 0) +// } yield { +// JSONAssert.assertEquals(expected, historyAfterCreation.toString, JSONCompareMode.LENIENT) +// } +// } +// } +// } + +// @RunWith(classOf[VertxUnitRunner]) +// class CreateHistoryCompatibilityTest extends LinkTestBase with TestHelper { +// // For migrated systems it is necessary to also write a history entry for a currently existing cell value + +// @Test +// def changeSimpleValue_firstChangeWithHistoryFeature_shouldCreateInitialHistoryEntry(implicit c: TestContext): Unit = { +// okTest { + +// val sqlConnection = SQLConnection(this.vertxAccess(), databaseConfig) +// val dbConnection = DatabaseConnection(this.vertxAccess(), sqlConnection) + +// val initialValue = """{ "value": "value before history feature" }""" +// val firstChangedValue = """{ "value": "my first change with history feature" }""" + +// for { +// _ <- createEmptyDefaultTable() +// _ <- sendRequest("POST", "/tables/1/rows") + +// // manually insert a value that simulates cell value changes before implementation of the history feature +// _ <- dbConnection.query("""UPDATE +// |user_table_1 +// |SET column_1 = 'value before history feature' +// |WHERE id = 1""".stripMargin) + +// _ <- sendRequest("POST", "/tables/1/columns/1/rows/1", firstChangedValue) +// rows <- sendRequest("GET", "/tables/1/columns/1/rows/1/history?historyType=cell").map(toRowsArray) + +// initialHistoryCreation = rows.get[JsonObject](0) +// firstHistoryCreation = rows.get[JsonObject](1) +// } yield { +// JSONAssert.assertEquals(initialValue, initialHistoryCreation.toString, JSONCompareMode.LENIENT) +// JSONAssert.assertEquals(firstChangedValue, firstHistoryCreation.toString, JSONCompareMode.LENIENT) +// } +// } +// } + +// @Test +// def changeSimpleValue_secondChangeWithHistoryFeature_shouldAgainCreateSingleHistoryEntries( +// implicit c: TestContext +// ): Unit = { +// okTest { +// val sqlConnection = SQLConnection(this.vertxAccess(), databaseConfig) +// val dbConnection = DatabaseConnection(this.vertxAccess(), sqlConnection) + +// val initialValue = """{ "value": "value before history feature" }""" +// val change1 = """{ "value": "first change" }""" +// val change2 = """{ "value": "second change" }""" + +// for { +// _ <- createEmptyDefaultTable() +// _ <- sendRequest("POST", "/tables/1/rows") + +// // manually insert a value that simulates cell value changes before implementation of the history feature +// _ <- dbConnection.query("""UPDATE +// |user_table_1 +// |SET column_1 = 'value before history feature' +// |WHERE id = 1""".stripMargin) + +// _ <- sendRequest("POST", "/tables/1/columns/1/rows/1", change1) +// _ <- sendRequest("POST", "/tables/1/columns/1/rows/1", change2) +// rows <- sendRequest("GET", "/tables/1/columns/1/rows/1/history?historyType=cell").map(toRowsArray) + +// initialHistory = rows.get[JsonObject](0) +// history1 = rows.get[JsonObject](1) +// history2 = rows.get[JsonObject](2) +// } yield { +// JSONAssert.assertEquals(initialValue, initialHistory.toString, JSONCompareMode.LENIENT) +// JSONAssert.assertEquals(change1, history1.toString, JSONCompareMode.LENIENT) +// JSONAssert.assertEquals(change2, history2.toString, JSONCompareMode.LENIENT) +// } +// } +// } + +// @Test +// def changeMultilanguageValue_firstChangeWithHistoryFeature_shouldCreateInitialHistoryEntry( +// implicit c: TestContext +// ): Unit = { +// okTest { +// val sqlConnection = SQLConnection(this.vertxAccess(), databaseConfig) +// val dbConnection = DatabaseConnection(this.vertxAccess(), sqlConnection) + +// val initialValueDE = """{ "value": { "de-DE": "de-DE init" } }""" +// val initialValueEN = """{ "value": { "en-GB": "en-GB init" } }""" +// val change1 = """{ "value": { "de-DE": "de-DE first change" } }""" +// val change2 = """{ "value": { "de-DE": "de-DE second change" } }""" +// val change3 = """{ "value": { "en-GB": "en-GB first change" } }""" + +// for { +// _ <- createTableWithMultilanguageColumns("history test") +// _ <- sendRequest("POST", "/tables/1/rows") + +// // manually insert a value that simulates cell value changes before implementation of the history feature +// _ <- dbConnection.query("""INSERT INTO user_table_lang_1(id, langtag,column_1) +// | VALUES +// |(1, E'de-DE', E'de-DE init'), +// |(1, E'en-GB', E'en-GB init') +// |""".stripMargin) + +// _ <- sendRequest("POST", "/tables/1/columns/1/rows/1", change1) +// _ <- sendRequest("POST", "/tables/1/columns/1/rows/1", change2) +// _ <- sendRequest("POST", "/tables/1/columns/1/rows/1", change3) +// rows <- sendRequest("GET", "/tables/1/columns/1/rows/1/history?historyType=cell").map(toRowsArray) + +// initialHistoryDE = rows.getJsonObject(0) +// history1 = rows.getJsonObject(1) +// history2 = rows.getJsonObject(2) +// initialHistoryEN = rows.getJsonObject(3) +// history3 = rows.getJsonObject(4) +// } yield { +// JSONAssert.assertEquals(initialValueDE, initialHistoryDE.toString, JSONCompareMode.LENIENT) +// JSONAssert.assertEquals(change1, history1.toString, JSONCompareMode.LENIENT) +// JSONAssert.assertEquals(change2, history2.toString, JSONCompareMode.LENIENT) +// JSONAssert.assertEquals(initialValueEN, initialHistoryEN.toString, JSONCompareMode.LENIENT) +// JSONAssert.assertEquals(change3, history3.toString, JSONCompareMode.LENIENT) +// } +// } +// } + +// @Test +// def changeCurrencyPOST_firstChangeWithHistoryFeature_shouldCreateInitialHistoryEntry( +// implicit c: TestContext +// ): Unit = { +// okTest { +// val sqlConnection = SQLConnection(this.vertxAccess(), databaseConfig) +// val dbConnection = DatabaseConnection(this.vertxAccess(), sqlConnection) + +// val expectedValues = +// """[ +// | {"value": {"DE": 11}}, +// | {"value": {"GB": 22}}, +// | {"value": {"DE": 33}}, +// | {"value": {"GB": 44}} +// |]""".stripMargin + +// val multiCountryCurrencyColumn = MultiCountry(CurrencyCol("currency-column"), Seq("DE", "GB")) + +// for { +// _ <- createSimpleTableWithCell("table1", multiCountryCurrencyColumn) + +// // manually insert a value that simulates cell value changes before implementation of the history feature +// _ <- dbConnection.query("""INSERT INTO user_table_lang_1(id, langtag, column_1) +// | VALUES +// |(1, E'DE', 11), +// |(1, E'GB', 22) +// |""".stripMargin) + +// _ <- sendRequest("POST", s"/tables/1/columns/1/rows/1", """{"value": {"DE": 33, "GB": 44}}""") +// rows <- sendRequest("GET", "/tables/1/columns/1/rows/1/history?historyType=cell").map(toRowsArray) +// } yield { +// JSONAssert.assertEquals(expectedValues, rows.toString, JSONCompareMode.LENIENT) +// } +// } +// } + +// @Test +// def changeCurrencyPUT_firstChangeWithHistoryFeature_shouldCreateInitialHistoryEntry(implicit c: TestContext): Unit = { +// okTest { +// val sqlConnection = SQLConnection(this.vertxAccess(), databaseConfig) +// val dbConnection = DatabaseConnection(this.vertxAccess(), sqlConnection) + +// val expectedValues = +// """[ +// | {"value": {"DE": 11}}, +// | {"value": {"GB": 22}}, +// | {"value": {"DE": 33}}, +// | {"value": {"GB": 44}} +// |]""".stripMargin + +// val multiCountryCurrencyColumn = MultiCountry(CurrencyCol("currency-column"), Seq("DE", "GB")) + +// for { +// _ <- createSimpleTableWithCell("table1", multiCountryCurrencyColumn) + +// // manually insert a value that simulates cell value changes before implementation of the history feature +// _ <- dbConnection.query("""INSERT INTO user_table_lang_1(id, langtag, column_1) +// | VALUES +// |(1, E'DE', 11), +// |(1, E'GB', 22) +// |""".stripMargin) + +// _ <- sendRequest("PUT", s"/tables/1/columns/1/rows/1", """{"value": {"DE": 33, "GB": 44}}""") +// rows <- sendRequest("GET", "/tables/1/columns/1/rows/1/history?historyType=cell").map(toRowsArray) +// } yield { +// JSONAssert.assertEquals(expectedValues, rows.toString, JSONCompareMode.LENIENT) +// } +// } +// } + +// @Test +// def changeLinkValue_firstChangeWithHistoryFeature_shouldCreateInitialHistoryEntry(implicit c: TestContext): Unit = { +// okTest { +// val sqlConnection = SQLConnection(this.vertxAccess(), databaseConfig) +// val dbConnection = DatabaseConnection(this.vertxAccess(), sqlConnection) + +// val expectedInitialLinks = """{ "value": [ {"id":3}, {"id": 4} ] }""" +// val expectedAfterPostLinks = """{ "value": [ {"id":3}, {"id": 4}, {"id": 5} ] }""" + +// for { +// linkColumnId <- setupTwoTablesWithEmptyLinks() + +// _ <- dbConnection.query("""INSERT INTO link_table_1 +// | (id_1, id_2) +// |VALUES +// | (1, 3), +// | (1, 4) +// | """.stripMargin) + +// _ <- sendRequest("POST", s"/tables/1/columns/$linkColumnId/rows/1", """{ "value": [ 5 ] }""") +// rows <- sendRequest("GET", s"/tables/1/columns/$linkColumnId/rows/1/history?historyType=cell").map(toRowsArray) +// initialHistory = rows.getJsonObject(0) +// history1 = rows.getJsonObject(1) +// } yield { +// JSONAssert.assertEquals(expectedInitialLinks, initialHistory.toString, JSONCompareMode.LENIENT) +// JSONAssert.assertEquals(expectedAfterPostLinks, history1.toString, JSONCompareMode.LENIENT) +// } +// } +// } + +// @Test +// def changeLinkValue_twoLinkChanges_onlyFirstOneShouldCreateInitialHistoryEntry(implicit c: TestContext): Unit = { +// okTest { +// val sqlConnection = SQLConnection(this.vertxAccess(), databaseConfig) +// val dbConnection = DatabaseConnection(this.vertxAccess(), sqlConnection) + +// val expectedInitialLinks = """{ "value": [ {"id":3} ] }""" +// val expectedAfterPostLinks1 = """{ "value": [ {"id":3}, {"id":4} ] }""" +// val expectedAfterPostLinks2 = """{ "value": [ {"id":3}, {"id":4}, {"id":5} ] }""" + +// for { +// linkColumnId <- setupTwoTablesWithEmptyLinks() + +// _ <- dbConnection.query("""INSERT INTO link_table_1 +// | (id_1, id_2) +// |VALUES +// | (1, 3) +// | """.stripMargin) + +// _ <- sendRequest("POST", s"/tables/1/columns/$linkColumnId/rows/1", """{ "value": [ 4 ] }""") +// _ <- sendRequest("POST", s"/tables/1/columns/$linkColumnId/rows/1", """{ "value": [ 5 ] }""") +// rows <- sendRequest("GET", s"/tables/1/columns/$linkColumnId/rows/1/history?historyType=cell").map(toRowsArray) + +// initialHistory = rows.getJsonObject(0) +// history1 = rows.getJsonObject(1) +// history2 = rows.getJsonObject(2) +// } yield { +// JSONAssert.assertEquals(expectedInitialLinks, initialHistory.toString, JSONCompareMode.LENIENT) +// JSONAssert.assertEquals(expectedAfterPostLinks1, history1.toString, JSONCompareMode.LENIENT) +// JSONAssert.assertEquals(expectedAfterPostLinks2, history2.toString, JSONCompareMode.LENIENT) +// } +// } +// } + +// @Test +// def deleteLinkValue_threeLinks_deleteOneOfThem(implicit c: TestContext): Unit = { +// okTest { +// val sqlConnection = SQLConnection(this.vertxAccess(), databaseConfig) +// val dbConnection = DatabaseConnection(this.vertxAccess(), sqlConnection) + +// val expectedInitialLinks = """{ "value": [ {"id":3}, {"id":4}, {"id":5} ] }""" +// val expectedAfterPostLinks1 = """{ "value": [ {"id":3}, {"id":5} ] }""" + +// for { +// linkColumnId <- setupTwoTablesWithEmptyLinks() + +// _ <- dbConnection.query("""INSERT INTO link_table_1 +// | (id_1, id_2) +// |VALUES +// | (1, 3), +// | (1, 4), +// | (1, 5) +// | """.stripMargin) + +// _ <- sendRequest("DELETE", s"/tables/1/columns/$linkColumnId/rows/1/link/4") +// rows <- sendRequest("GET", s"/tables/1/columns/$linkColumnId/rows/1/history?historyType=cell").map(toRowsArray) + +// initialHistory = rows.getJsonObject(0) +// history1 = rows.getJsonObject(1) +// } yield { +// JSONAssert.assertEquals(expectedInitialLinks, initialHistory.toString, JSONCompareMode.LENIENT) +// JSONAssert.assertEquals(expectedAfterPostLinks1, history1.toString, JSONCompareMode.LENIENT) +// } +// } +// } + +// @Test +// def changeLinkOrder_reverseOrderInTwoSteps_createOnlyOneInitHistory(implicit c: TestContext): Unit = { +// okTest { +// val sqlConnection = SQLConnection(this.vertxAccess(), databaseConfig) +// val dbConnection = DatabaseConnection(this.vertxAccess(), sqlConnection) + +// val expectedInitialLinks = """{ "value": [ {"id":3}, {"id":4}, {"id":5} ] }""" +// val expectedAfterPostLinks1 = """{ "value": [ {"id":4}, {"id":5}, {"id":3} ] }""" +// val expectedAfterPostLinks2 = """{ "value": [ {"id":5}, {"id":4}, {"id":3} ] }""" + +// for { +// linkColumnId <- setupTwoTablesWithEmptyLinks() + +// _ <- dbConnection.query("""INSERT INTO link_table_1 +// | (id_1, id_2) +// |VALUES +// | (1, 3), +// | (1, 4), +// | (1, 5) +// | """.stripMargin) + +// _ <- sendRequest("PUT", s"/tables/1/columns/$linkColumnId/rows/1/link/3/order", s""" {"location": "end"} """) +// _ <- sendRequest("PUT", s"/tables/1/columns/$linkColumnId/rows/1/link/5/order", s""" {"location": "start"} """) + +// rows <- sendRequest("GET", s"/tables/1/columns/$linkColumnId/rows/1/history?historyType=cell").map(toRowsArray) + +// initialHistory = rows.getJsonObject(0) +// history1 = rows.getJsonObject(1) +// history2 = rows.getJsonObject(2) +// } yield { +// JSONAssert.assertEquals(expectedInitialLinks, initialHistory.toString, JSONCompareMode.LENIENT) +// JSONAssert.assertEquals(expectedAfterPostLinks1, history1.toString, JSONCompareMode.LENIENT) +// JSONAssert.assertEquals(expectedAfterPostLinks2, history2.toString, JSONCompareMode.LENIENT) +// } +// } +// } + +// @Test +// def deleteLinkValue_threeLinks_deleteTwoTimesOnlyOneInitHistory(implicit c: TestContext): Unit = { +// okTest { +// val sqlConnection = SQLConnection(this.vertxAccess(), databaseConfig) +// val dbConnection = DatabaseConnection(this.vertxAccess(), sqlConnection) + +// val expectedInitialLinks = """{ "value": [ {"id":3}, {"id":4}, {"id":5} ] }""" +// val expectedAfterPostLinks1 = """{ "value": [ {"id":3}, {"id":5} ] }""" +// val expectedAfterPostLinks2 = """{ "value": [ {"id":5} ] }""" + +// for { +// linkColumnId <- setupTwoTablesWithEmptyLinks() + +// _ <- dbConnection.query("""INSERT INTO link_table_1 +// | (id_1, id_2) +// |VALUES +// | (1, 3), +// | (1, 4), +// | (1, 5) +// | """.stripMargin) + +// _ <- sendRequest("DELETE", s"/tables/1/columns/$linkColumnId/rows/1/link/4") +// _ <- sendRequest("DELETE", s"/tables/1/columns/$linkColumnId/rows/1/link/3") +// rows <- sendRequest("GET", s"/tables/1/columns/$linkColumnId/rows/1/history?historyType=cell").map(toRowsArray) + +// initialHistory = rows.getJsonObject(0) +// history1 = rows.getJsonObject(1) +// history2 = rows.getJsonObject(2) +// } yield { +// JSONAssert.assertEquals(expectedInitialLinks, initialHistory.toString, JSONCompareMode.LENIENT) +// JSONAssert.assertEquals(expectedAfterPostLinks1, history1.toString, JSONCompareMode.LENIENT) +// JSONAssert.assertEquals(expectedAfterPostLinks2, history2.toString, JSONCompareMode.LENIENT) +// } +// } +// } + +// @Test +// def changeSimpleValue_booleanInitHistoryWithValueFalse(implicit c: TestContext): Unit = { +// okTest { +// // Booleans always gets a initial history entry on first change +// val expectedInitialLinks = """{ "value": false} """ +// val expectedAfterPost1 = """{ "value": true }""" + +// val booleanColumn = +// s"""{"columns": [{"kind": "boolean", "name": "Boolean Column", "languageType": "neutral"} ] }""" + +// for { +// _ <- createEmptyDefaultTable("history test") + +// // create simple boolean column +// _ <- sendRequest("POST", "/tables/1/columns", booleanColumn) + +// _ <- sendRequest("POST", "/tables/1/rows") +// _ <- sendRequest("POST", "/tables/1/columns/3/rows/1", expectedAfterPost1) +// rows <- sendRequest("GET", "/tables/1/columns/3/rows/1/history?historyType=cell").map(toRowsArray) + +// initialHistory = rows.getJsonObject(0) +// history1 = rows.getJsonObject(1) +// } yield { +// JSONAssert.assertEquals(expectedInitialLinks, initialHistory.toString, JSONCompareMode.LENIENT) +// JSONAssert.assertEquals(expectedAfterPost1, history1.toString, JSONCompareMode.LENIENT) +// } +// } +// } + +// @Test +// def changeSimpleValue_booleanInitHistoryWithSameValue(implicit c: TestContext): Unit = { +// okTest { +// val sqlConnection = SQLConnection(this.vertxAccess(), databaseConfig) +// val dbConnection = DatabaseConnection(this.vertxAccess(), sqlConnection) + +// // Booleans always gets a initial history entry on first change +// val expectedInitialLinks = """{ "value": true} """ +// val expectedAfterPost1 = """{ "value": true }""" + +// val booleanColumn = +// s"""{"columns": [{"kind": "boolean", "name": "Boolean Column", "languageType": "neutral"} ] }""" + +// for { +// _ <- createEmptyDefaultTable("history test") + +// // create simple boolean column +// _ <- sendRequest("POST", "/tables/1/columns", booleanColumn) + +// _ <- sendRequest("POST", "/tables/1/rows") + +// // manually update value that simulates cell value changes before implementation of the history feature +// _ <- dbConnection.query("""UPDATE user_table_1 SET column_3 = TRUE WHERE id = 1""".stripMargin) + +// _ <- sendRequest("POST", "/tables/1/columns/3/rows/1", expectedAfterPost1) +// rows <- sendRequest("GET", "/tables/1/columns/3/rows/1/history?historyType=cell").map(toRowsArray) + +// initialHistory = rows.getJsonObject(0) +// history1 = rows.getJsonObject(1) +// } yield { +// JSONAssert.assertEquals(expectedInitialLinks, initialHistory.toString, JSONCompareMode.LENIENT) +// JSONAssert.assertEquals(expectedAfterPost1, history1.toString, JSONCompareMode.LENIENT) +// } +// } +// } + +// @Test +// def changeSimpleValue_boolean(implicit c: TestContext): Unit = { +// okTest { +// // Booleans always gets a initial history entry on first change +// val expectedInitialLinks = """{ "value": false} """ +// val expectedAfterPost1 = """{ "value": true }""" +// val expectedAfterPost2 = """{ "value": false }""" + +// val booleanColumn = +// s"""{"columns": [{"kind": "boolean", "name": "Boolean Column", "languageType": "neutral"} ] }""" + +// for { +// _ <- createEmptyDefaultTable("history test") + +// // create simple boolean column +// _ <- sendRequest("POST", "/tables/1/columns", booleanColumn) + +// _ <- sendRequest("POST", "/tables/1/rows") +// _ <- sendRequest("POST", "/tables/1/columns/3/rows/1", expectedAfterPost1) +// _ <- sendRequest("POST", "/tables/1/columns/3/rows/1", expectedAfterPost2) +// rows <- sendRequest("GET", "/tables/1/columns/3/rows/1/history?historyType=cell").map(toRowsArray) + +// initialHistory = rows.getJsonObject(0) +// history1 = rows.getJsonObject(1) +// history2 = rows.getJsonObject(2) +// } yield { +// JSONAssert.assertEquals(expectedInitialLinks, initialHistory.toString, JSONCompareMode.LENIENT) +// JSONAssert.assertEquals(expectedAfterPost1, history1.toString, JSONCompareMode.LENIENT) +// JSONAssert.assertEquals(expectedAfterPost2, history2.toString, JSONCompareMode.LENIENT) +// } +// } +// } + +// @Test +// def changeMultilanguageValue_boolean(implicit c: TestContext): Unit = { +// okTest { +// // Booleans always gets a initial history entry on first change +// val expectedInitialLinks = """{ "value": {"de-DE": false} }""" +// val expectedAfterPost1 = """{ "value": {"de-DE": true} }""" +// val expectedAfterPost2 = """{ "value": {"de-DE": false} }""" + +// for { +// _ <- createTableWithMultilanguageColumns("history test") +// _ <- sendRequest("POST", "/tables/1/rows") +// _ <- sendRequest("POST", "/tables/1/columns/2/rows/1", expectedAfterPost1) +// _ <- sendRequest("POST", "/tables/1/columns/2/rows/1", expectedAfterPost2) +// rows <- sendRequest("GET", "/tables/1/columns/2/rows/1/history?historyType=cell").map(toRowsArray) + +// initialHistory = rows.getJsonObject(0) +// history1 = rows.getJsonObject(1) +// history2 = rows.getJsonObject(2) +// } yield { +// JSONAssert.assertEquals(expectedInitialLinks, initialHistory.toString, JSONCompareMode.LENIENT) +// JSONAssert.assertEquals(expectedAfterPost1, history1.toString, JSONCompareMode.LENIENT) +// JSONAssert.assertEquals(expectedAfterPost2, history2.toString, JSONCompareMode.LENIENT) +// } +// } +// } + +// @Test +// def deleteSimpleCell_firstChangeWithHistoryFeature_shouldCreateInitialHistoryEntry(implicit c: TestContext): Unit = { +// okTest { + +// val sqlConnection = SQLConnection(this.vertxAccess(), databaseConfig) +// val dbConnection = DatabaseConnection(this.vertxAccess(), sqlConnection) + +// val initialValue = """{ "value": "value before history feature" }""" + +// for { +// _ <- createEmptyDefaultTable() +// _ <- sendRequest("POST", "/tables/1/rows") + +// // manually insert a value that simulates cell value changes before implementation of the history feature +// _ <- dbConnection.query("""UPDATE +// |user_table_1 +// |SET column_1 = 'value before history feature' +// |WHERE id = 1""".stripMargin) + +// _ <- sendRequest("DELETE", "/tables/1/columns/1/rows/1") +// rows <- sendRequest("GET", "/tables/1/columns/1/rows/1/history?historyType=cell").map(toRowsArray) + +// initialHistoryCreation = rows.get[JsonObject](0) +// firstHistoryCreation = rows.get[JsonObject](1) +// } yield { +// JSONAssert.assertEquals(initialValue, initialHistoryCreation.toString, JSONCompareMode.LENIENT) +// JSONAssert.assertEquals("""{ "value": null }""", firstHistoryCreation.toString, JSONCompareMode.LENIENT) +// } +// } +// } + +// @Test +// def deleteMultilanguageCell_firstChangeWithHistoryFeature_shouldCreateInitialHistoryEntry( +// implicit c: TestContext +// ): Unit = { +// okTest { +// val sqlConnection = SQLConnection(this.vertxAccess(), databaseConfig) +// val dbConnection = DatabaseConnection(this.vertxAccess(), sqlConnection) + +// val initialValueDE = """{ "value": { "de-DE": "de-DE init" } }""" + +// for { +// _ <- createTableWithMultilanguageColumns("history test") +// _ <- sendRequest("POST", "/tables/1/rows") + +// // manually insert a value that simulates cell value changes before implementation of the history feature +// _ <- dbConnection.query("""INSERT INTO user_table_lang_1(id, langtag, column_1) +// | VALUES +// |(1, E'de-DE', E'de-DE init'), +// |(1, E'en-GB', E'en-GB init') +// |""".stripMargin) + +// _ <- sendRequest("DELETE", "/tables/1/columns/1/rows/1") +// rows <- sendRequest("GET", "/tables/1/columns/1/rows/1/history/de-DE?historyType=cell").map(toRowsArray) + +// initialHistoryDE = rows.getJsonObject(0) +// history = rows.getJsonObject(1) +// } yield { +// JSONAssert.assertEquals(initialValueDE, initialHistoryDE.toString, JSONCompareMode.LENIENT) +// JSONAssert.assertEquals("""{ "value": { "de-DE": null } }""", history.toString, JSONCompareMode.LENIENT) +// } +// } +// } + +// @Test +// def deleteLinkCell_threeLinks_deleteTwoTimesOnlyOneInitHistory(implicit c: TestContext): Unit = { +// okTest { +// val sqlConnection = SQLConnection(this.vertxAccess(), databaseConfig) +// val dbConnection = DatabaseConnection(this.vertxAccess(), sqlConnection) + +// val expectedInitialLinks = """[ {"id":3}, {"id":4}, {"id":5} ]""" + +// for { +// linkColumnId <- setupTwoTablesWithEmptyLinks() + +// // manually insert a value that simulates cell value changes before implementation of the history feature +// _ <- dbConnection.query("""INSERT INTO link_table_1 +// | (id_1, id_2) +// |VALUES +// | (1, 3), +// | (1, 4), +// | (1, 5) +// | """.stripMargin) + +// _ <- sendRequest("DELETE", s"/tables/1/columns/$linkColumnId/rows/1") +// rows <- sendRequest("GET", "/tables/1/columns/3/rows/1/history?historyType=cell").map(toRowsArray) +// initialHistory = getLinksValue(rows, 0) +// history = getLinksValue(rows, 1) +// } yield { +// JSONAssert.assertEquals(expectedInitialLinks, initialHistory.toString, JSONCompareMode.LENIENT) +// JSONAssert.assertEquals("[]", history.toString, JSONCompareMode.LENIENT) +// } +// } +// } + +// @Test +// def addAttachment_firstChangeWithHistoryFeature_shouldCreateInitialHistoryEntry(implicit c: TestContext): Unit = { +// okTest { +// val sqlConnection = SQLConnection(this.vertxAccess(), databaseConfig) +// val dbConnection = DatabaseConnection(this.vertxAccess(), sqlConnection) +// val attachmentColumn = """{"columns": [{"kind": "attachment", "name": "Downloads"}] }""" + +// for { +// _ <- createDefaultTable() +// _ <- sendRequest("POST", s"/tables/1/columns", attachmentColumn) + +// fileUuid1 <- createTestAttachment("Test 1") +// fileUuid2 <- createTestAttachment("Test 2") + +// // manually insert a value that simulates cell value changes before implementation of the history feature +// _ <- dbConnection.query(s"""INSERT INTO system_attachment +// | (table_id, column_id, row_id, attachment_uuid, ordering) +// |VALUES +// | (1, 3, 1, '$fileUuid1', 1) +// | """.stripMargin) + +// _ <- sendRequest("POST", s"/tables/1/columns/3/rows/1", Json.obj("value" -> Json.obj("uuid" -> fileUuid2))) +// rows <- sendRequest("GET", "/tables/1/columns/3/rows/1/history?historyType=cell").map(toRowsArray) +// initialHistory = rows.get[JsonObject](0).getJsonArray("value").getJsonObject(0) +// history = rows.get[JsonObject](1).getJsonArray("value").getJsonObject(0) +// } yield { +// assertJSONEquals(Json.obj("uuid" -> fileUuid1), initialHistory, JSONCompareMode.LENIENT) +// assertJSONEquals(Json.obj("uuid" -> fileUuid1), history, JSONCompareMode.LENIENT) +// } +// } +// } + +// @Test +// def addAttachment_twoTimes_shouldOnlyCreateOneInitialHistoryEntry(implicit c: TestContext): Unit = { +// okTest { +// val sqlConnection = SQLConnection(this.vertxAccess(), databaseConfig) +// val dbConnection = DatabaseConnection(this.vertxAccess(), sqlConnection) +// val attachmentColumn = """{"columns": [{"kind": "attachment", "name": "Downloads"}] }""" + +// for { +// _ <- createDefaultTable() +// _ <- sendRequest("POST", s"/tables/1/columns", attachmentColumn) + +// fileUuid1 <- createTestAttachment("Test 1") +// fileUuid2 <- createTestAttachment("Test 2") +// fileUuid3 <- createTestAttachment("Test 3") + +// // manually insert a value that simulates cell value changes before implementation of the history feature +// _ <- dbConnection.query(s"""INSERT INTO system_attachment +// | (table_id, column_id, row_id, attachment_uuid, ordering) +// |VALUES +// | (1, 3, 1, '$fileUuid1', 1) +// | """.stripMargin) + +// _ <- sendRequest("POST", s"/tables/1/columns/3/rows/1", Json.obj("value" -> Json.obj("uuid" -> fileUuid2))) +// _ <- sendRequest("POST", s"/tables/1/columns/3/rows/1", Json.obj("value" -> Json.obj("uuid" -> fileUuid3))) +// rows <- sendRequest("GET", "/tables/1/columns/3/rows/1/history?historyType=cell").map(toRowsArray) +// } yield { +// assertEquals(3, rows.size()) +// } +// } +// } + +// @Test +// def deleteAttachmentCell_shouldCreateInitialHistoryEntry(implicit c: TestContext): Unit = { +// okTest { +// val sqlConnection = SQLConnection(this.vertxAccess(), databaseConfig) +// val dbConnection = DatabaseConnection(this.vertxAccess(), sqlConnection) +// val attachmentColumn = """{"columns": [{"kind": "attachment", "name": "Downloads"}] }""" + +// for { +// _ <- createDefaultTable() +// _ <- sendRequest("POST", s"/tables/1/columns", attachmentColumn) + +// fileUuid1 <- createTestAttachment("Test 1") +// fileUuid2 <- createTestAttachment("Test 2") + +// // manually insert a value that simulates cell value changes before implementation of the history feature +// _ <- dbConnection.query(s"""INSERT INTO system_attachment +// | (table_id, column_id, row_id, attachment_uuid, ordering) +// |VALUES +// | (1, 3, 1, '$fileUuid1', 1), +// | (1, 3, 1, '$fileUuid2', 2) +// | """.stripMargin) + +// _ <- sendRequest("DELETE", "/tables/1/columns/3/rows/1") + +// rows <- sendRequest("GET", "/tables/1/columns/3/rows/1/history?historyType=cell").map(toRowsArray) +// initialCountBeforeDeletion = rows.get[JsonObject](0).getJsonArray("value") +// attachmentCountAfterDeletion = rows.get[JsonObject](1).getJsonArray("value") +// } yield { +// assertEquals(2, initialCountBeforeDeletion.size()) +// assertEquals(0, attachmentCountAfterDeletion.size()) +// } +// } +// } + +// } + +// @RunWith(classOf[VertxUnitRunner]) +// class CreateAttachmentHistoryTest extends MediaTestBase with TestHelper { + +// @Test +// def addAttachment_toEmptyCell(implicit c: TestContext): Unit = { +// okTest { +// val attachmentColumn = """{"columns": [{"kind": "attachment", "name": "Downloads"}] }""" + +// for { +// _ <- createDefaultTable() +// _ <- sendRequest("POST", s"/tables/1/columns", attachmentColumn) + +// fileUuid <- createTestAttachment("Test 1") + +// _ <- sendRequest("POST", s"/tables/1/columns/3/rows/1", Json.obj("value" -> Json.obj("uuid" -> fileUuid))) +// rows <- sendRequest("GET", "/tables/1/columns/3/rows/1/history?historyType=cell").map(toRowsArray) +// currentUuid = rows.get[JsonObject](0).getJsonArray("value").getJsonObject(0).getString("uuid") +// } yield { +// assertEquals(fileUuid, currentUuid) +// } +// } +// } + +// @Test +// def addAttachment_toCellContainingOneAttachment(implicit c: TestContext): Unit = { +// okTest { +// val attachmentColumn = """{"columns": [{"kind": "attachment", "name": "Downloads"}] }""" + +// for { +// _ <- createDefaultTable() +// _ <- sendRequest("POST", s"/tables/1/columns", attachmentColumn) + +// fileUuid1 <- createTestAttachment("Test 1") +// fileUuid2 <- createTestAttachment("Test 2") + +// _ <- sendRequest("POST", s"/tables/1/columns/3/rows/1", Json.obj("value" -> Json.obj("uuid" -> fileUuid1))) +// _ <- sendRequest("POST", s"/tables/1/columns/3/rows/1", Json.obj("value" -> Json.obj("uuid" -> fileUuid2))) +// rows <- sendRequest("GET", "/tables/1/columns/3/rows/1/history?historyType=cell").map(toRowsArray) +// firstHistory = rows.get[JsonObject](0).getJsonArray("value").getJsonObject(0) +// secondHistory1 = rows.get[JsonObject](1).getJsonArray("value").getJsonObject(0) +// secondHistory2 = rows.get[JsonObject](1).getJsonArray("value").getJsonObject(1) +// } yield { +// assertJSONEquals(Json.obj("uuid" -> fileUuid1), firstHistory, JSONCompareMode.LENIENT) +// assertJSONEquals(Json.obj("uuid" -> fileUuid1), secondHistory1, JSONCompareMode.LENIENT) +// assertJSONEquals(Json.obj("uuid" -> fileUuid2), secondHistory2, JSONCompareMode.LENIENT) +// } +// } +// } + +// @Test +// def addAttachments_addThreeAttachmentsAtOnce(implicit c: TestContext): Unit = { +// okTest { +// val attachmentColumn = """{"columns": [{"kind": "attachment", "name": "Downloads"}] }""" + +// for { +// _ <- createDefaultTable() +// _ <- sendRequest("POST", s"/tables/1/columns", attachmentColumn) + +// fileUuid1 <- createTestAttachment("Test 1") +// fileUuid2 <- createTestAttachment("Test 2") + +// _ <- sendRequest("POST", s"/tables/1/columns/3/rows/1", Json.obj("value" -> Json.arr(fileUuid1, fileUuid2))) + +// rows <- sendRequest("GET", "/tables/1/columns/3/rows/1/history?historyType=cell").map(toRowsArray) +// history1 = rows.get[JsonObject](0).getJsonArray("value").getJsonObject(0) +// history2 = rows.get[JsonObject](0).getJsonArray("value").getJsonObject(1) +// } yield { +// assertJSONEquals(Json.obj("uuid" -> fileUuid1), history1, JSONCompareMode.LENIENT) +// assertJSONEquals(Json.obj("uuid" -> fileUuid2), history2, JSONCompareMode.LENIENT) +// } +// } +// } + +// @Test +// def deleteAttachment_fromCellContainingTwoAttachments(implicit c: TestContext): Unit = { +// okTest { +// val attachmentColumn = """{"columns": [{"kind": "attachment", "name": "Downloads"}] }""" + +// for { +// _ <- createDefaultTable() +// _ <- sendRequest("POST", s"/tables/1/columns", attachmentColumn) + +// fileUuid1 <- createTestAttachment("Test 1") +// fileUuid2 <- createTestAttachment("Test 2") + +// _ <- sendRequest("POST", s"/tables/1/columns/3/rows/1", Json.obj("value" -> Json.arr(fileUuid1, fileUuid2))) +// _ <- sendRequest("DELETE", s"/tables/1/columns/3/rows/1/attachment/$fileUuid1") + +// rows <- sendRequest("GET", "/tables/1/columns/3/rows/1/history?historyType=cell").map(toRowsArray) + +// afterDeletionHistory = rows.get[JsonObject](1).getJsonArray("value").getJsonObject(0) +// } yield { + +// assertJSONEquals(Json.obj("uuid" -> fileUuid2), afterDeletionHistory, JSONCompareMode.LENIENT) +// } +// } +// } + +// @Test +// def deleteAttachment_fromCellContainingThreeAttachments(implicit c: TestContext): Unit = { +// okTest { +// val attachmentColumn = """{"columns": [{"kind": "attachment", "name": "Downloads"}] }""" + +// for { +// _ <- createDefaultTable() +// _ <- sendRequest("POST", s"/tables/1/columns", attachmentColumn) + +// fileUuid1 <- createTestAttachment("Test 1") +// fileUuid2 <- createTestAttachment("Test 2") +// fileUuid3 <- createTestAttachment("Test 3") + +// _ <- sendRequest( +// "POST", +// s"/tables/1/columns/3/rows/1", +// Json.obj("value" -> Json.arr(fileUuid1, fileUuid2, fileUuid3)) +// ) +// _ <- sendRequest("DELETE", s"/tables/1/columns/3/rows/1/attachment/$fileUuid2") + +// rows <- sendRequest("GET", "/tables/1/columns/3/rows/1/history?historyType=cell").map(toRowsArray) + +// afterDeletionHistory1 = rows.get[JsonObject](1).getJsonArray("value").getJsonObject(0) +// afterDeletionHistory2 = rows.get[JsonObject](1).getJsonArray("value").getJsonObject(1) +// } yield { +// assertJSONEquals(Json.obj("uuid" -> fileUuid1), afterDeletionHistory1, JSONCompareMode.LENIENT) +// assertJSONEquals(Json.obj("uuid" -> fileUuid3), afterDeletionHistory2, JSONCompareMode.LENIENT) +// } +// } +// } + +// @Test +// def deleteCell_attachment(implicit c: TestContext): Unit = { +// okTest { +// val attachmentColumn = """{"columns": [{"kind": "attachment", "name": "Downloads"}] }""" + +// for { +// _ <- createDefaultTable() +// _ <- sendRequest("POST", s"/tables/1/columns", attachmentColumn) + +// fileUuid1 <- createTestAttachment("Test 1") +// fileUuid2 <- createTestAttachment("Test 2") +// _ <- sendRequest("POST", s"/tables/1/columns/3/rows/1", Json.obj("value" -> Json.arr(fileUuid1, fileUuid2))) + +// _ <- sendRequest("DELETE", "/tables/1/columns/3/rows/1") + +// rows <- sendRequest("GET", "/tables/1/columns/3/rows/1/history?historyType=cell").map(toRowsArray) +// attachmentCountBeforeDeletion = rows.get[JsonObject](0).getJsonArray("value") +// attachmentCountAfterDeletion = rows.get[JsonObject](1).getJsonArray("value") +// } yield { +// assertEquals(2, attachmentCountBeforeDeletion.size()) +// assertEquals(0, attachmentCountAfterDeletion.size()) +// } +// } +// } +// } + +// @RunWith(classOf[VertxUnitRunner]) +// class CreateBidirectionalCompatibilityLinkHistoryTest extends LinkTestBase with TestHelper { + +// @Test +// def changeLink_twoLinksExisting_postOtherLink(implicit c: TestContext): Unit = { +// okTest { +// val sqlConnection = SQLConnection(this.vertxAccess(), databaseConfig) +// val dbConnection = DatabaseConnection(this.vertxAccess(), sqlConnection) + +// val expectedBacklink = """[{"id": 1, "value": "table1row1"}]""" + +// for { +// linkColumnId <- setupTwoTablesWithEmptyLinks() + +// _ <- dbConnection.query("""INSERT INTO link_table_1 +// | (id_1, id_2) +// |VALUES +// | (1, 3), +// | (1, 4) +// | """.stripMargin) + +// _ <- sendRequest("POST", s"/tables/1/columns/$linkColumnId/rows/1", """{ "value": [ 5 ] }""") +// backLinkRow3 <- sendRequest("GET", "/tables/2/columns/3/rows/3/history?historyType=cell").map(toRowsArray) +// backLinkRow4 <- sendRequest("GET", "/tables/2/columns/3/rows/4/history?historyType=cell").map(toRowsArray) +// backLinkRow5 <- sendRequest("GET", "/tables/2/columns/3/rows/5/history?historyType=cell").map(toRowsArray) + +// } yield { +// assertEquals(1, backLinkRow3.size()) +// assertEquals(1, backLinkRow4.size()) +// assertEquals(1, backLinkRow5.size()) + +// JSONAssert.assertEquals(expectedBacklink, getLinksValue(backLinkRow3, 0).toString, JSONCompareMode.LENIENT) +// JSONAssert.assertEquals(expectedBacklink, getLinksValue(backLinkRow4, 0).toString, JSONCompareMode.LENIENT) +// JSONAssert.assertEquals(expectedBacklink, getLinksValue(backLinkRow5, 0).toString, JSONCompareMode.LENIENT) +// } +// } +// } + +// @Test +// def changeLink_twoLinksExisting_putOneOfThem(implicit c: TestContext): Unit = { +// okTest { +// val sqlConnection = SQLConnection(this.vertxAccess(), databaseConfig) +// val dbConnection = DatabaseConnection(this.vertxAccess(), sqlConnection) + +// val expectedBacklink = """[{"id": 1, "value": "table1row1"}]""" + +// for { +// linkColumnId <- setupTwoTablesWithEmptyLinks() + +// _ <- dbConnection.query("""INSERT INTO link_table_1 +// | (id_1, id_2) +// |VALUES +// | (1, 3), +// | (1, 4) +// | """.stripMargin) + +// _ <- sendRequest("PUT", s"/tables/1/columns/$linkColumnId/rows/1", """{ "value": [ 4 ] }""") +// backLinkRow3 <- sendRequest("GET", "/tables/2/columns/3/rows/3/history?historyType=cell").map(toRowsArray) +// backLinkRow4 <- sendRequest("GET", "/tables/2/columns/3/rows/4/history?historyType=cell").map(toRowsArray) +// } yield { +// assertEquals(2, backLinkRow3.size()) +// assertEquals(2, backLinkRow4.size()) + +// JSONAssert.assertEquals(expectedBacklink, getLinksValue(backLinkRow3, 0).toString, JSONCompareMode.LENIENT) +// JSONAssert.assertEquals("[]", getLinksValue(backLinkRow3, 1).toString, JSONCompareMode.LENIENT) + +// JSONAssert.assertEquals(expectedBacklink, getLinksValue(backLinkRow4, 0).toString, JSONCompareMode.LENIENT) +// JSONAssert.assertEquals(expectedBacklink, getLinksValue(backLinkRow4, 1).toString, JSONCompareMode.LENIENT) +// } +// } +// } + +// @Test +// def changeLink_twoLinksExisting_putEmptyLinkArray(implicit c: TestContext): Unit = { +// okTest { +// val sqlConnection = SQLConnection(this.vertxAccess(), databaseConfig) +// val dbConnection = DatabaseConnection(this.vertxAccess(), sqlConnection) + +// val expectedInitialBacklink = """[{"id": 1, "value": "table1row1"}]""" + +// for { +// _ <- setupTwoTablesWithEmptyLinks() + +// _ <- dbConnection.query("""INSERT INTO link_table_1 +// | (id_1, id_2) +// |VALUES +// | (1, 3), +// | (1, 4) +// | """.stripMargin) + +// _ <- sendRequest("PUT", s"/tables/1/columns/3/rows/1", """{ "value": [] }""") +// backLinkRow3 <- sendRequest("GET", "/tables/2/columns/3/rows/3/history?historyType=cell").map(toRowsArray) +// backLinkRow4 <- sendRequest("GET", "/tables/2/columns/3/rows/4/history?historyType=cell").map(toRowsArray) +// } yield { +// assertEquals(2, backLinkRow3.size()) +// assertEquals(2, backLinkRow4.size()) + +// JSONAssert +// .assertEquals(expectedInitialBacklink, getLinksValue(backLinkRow3, 0).toString, JSONCompareMode.LENIENT) +// JSONAssert.assertEquals("[]", getLinksValue(backLinkRow3, 1).toString, JSONCompareMode.LENIENT) + +// JSONAssert +// .assertEquals(expectedInitialBacklink, getLinksValue(backLinkRow4, 0).toString, JSONCompareMode.LENIENT) +// JSONAssert.assertEquals("[]", getLinksValue(backLinkRow4, 1).toString, JSONCompareMode.LENIENT) +// } +// } +// } + +// @Test +// def changeLinkOrder_reverseOrderInTwoSteps_shouldNotCreateBacklinkHistory(implicit c: TestContext): Unit = { +// okTest { +// val sqlConnection = SQLConnection(this.vertxAccess(), databaseConfig) +// val dbConnection = DatabaseConnection(this.vertxAccess(), sqlConnection) + +// for { +// _ <- setupTwoTablesWithEmptyLinks() + +// _ <- dbConnection.query("""INSERT INTO link_table_1 +// | (id_1, id_2) +// |VALUES +// | (1, 3), +// | (1, 4), +// | (1, 5) +// | """.stripMargin) + +// _ <- sendRequest("PUT", s"/tables/1/columns/3/rows/1/link/3/order", s""" {"location": "end"} """) +// _ <- sendRequest("PUT", s"/tables/1/columns/3/rows/1/link/5/order", s""" {"location": "start"} """) + +// backLinkRow3 <- sendRequest("GET", "/tables/2/columns/3/rows/3/history?historyType=cell").map(toRowsArray) +// backLinkRow4 <- sendRequest("GET", "/tables/2/columns/3/rows/4/history?historyType=cell").map(toRowsArray) +// } yield { +// assertEquals(1, backLinkRow3.size()) +// assertEquals(1, backLinkRow4.size()) +// } +// } +// } + +// @Test +// def patchLink_oneLinksExisting_deleteCell(implicit c: TestContext): Unit = { +// okTest { +// val sqlConnection = SQLConnection(this.vertxAccess(), databaseConfig) +// val dbConnection = DatabaseConnection(this.vertxAccess(), sqlConnection) + +// for { +// _ <- setupTwoTablesWithEmptyLinks() + +// _ <- dbConnection.query("""INSERT INTO link_table_1 +// | (id_1, id_2) +// |VALUES +// | (3, 3), (3, 4), (4, 3) +// | """.stripMargin) + +// _ <- sendRequest("PATCH", s"/tables/1/columns/3/rows/3", """{ "value": [ 5 ] }""") + +// t1r3 <- sendRequest("GET", "/tables/1/columns/3/rows/3/history?historyType=cell").map(toRowsArray) + +// t2r3 <- sendRequest("GET", "/tables/2/columns/3/rows/3/history?historyType=cell").map(toRowsArray) +// t2r4 <- sendRequest("GET", "/tables/2/columns/3/rows/4/history?historyType=cell").map(toRowsArray) +// t2r5 <- sendRequest("GET", "/tables/2/columns/3/rows/5/history?historyType=cell").map(toRowsArray) +// } yield { +// assertEquals(1, t2r3.size()) +// assertEquals(1, t2r4.size()) +// assertEquals(1, t2r5.size()) + +// assertEquals(2, t1r3.size()) + +// assertJSONEquals("""[{"id": 3}, {"id": 4}]""", getLinksValue(t2r3, 0).toString, JSONCompareMode.STRICT_ORDER) +// assertJSONEquals("""[{"id": 3}]""", getLinksValue(t2r4, 0).toString, JSONCompareMode.STRICT_ORDER) +// assertJSONEquals("""[{"id": 3}]""", getLinksValue(t2r5, 0).toString, JSONCompareMode.STRICT_ORDER) + +// assertJSONEquals("""[{"id": 3}, {"id": 4}]""", getLinksValue(t1r3, 0).toString, JSONCompareMode.STRICT_ORDER) +// assertJSONEquals( +// """[{"id": 3}, {"id": 4}, {"id": 5}]""", +// getLinksValue(t1r3, 1).toString, +// JSONCompareMode.STRICT_ORDER +// ) +// } +// } +// } + +// } From 5fc332d4cd9be46898b4dee8300a40be7f47f07e Mon Sep 17 00:00:00 2001 From: Manfred Zingl Date: Mon, 23 Oct 2023 15:48:23 +0200 Subject: [PATCH 3/9] add check for value changes --- .../database/model/TableauxModel.scala | 64 ++++++++++++++----- .../CreateHistoryChangeDetectionTest.scala | 59 +++++------------ 2 files changed, 64 insertions(+), 59 deletions(-) diff --git a/src/main/scala/com/campudus/tableaux/database/model/TableauxModel.scala b/src/main/scala/com/campudus/tableaux/database/model/TableauxModel.scala index 2d0fee1d..e2ad66e8 100644 --- a/src/main/scala/com/campudus/tableaux/database/model/TableauxModel.scala +++ b/src/main/scala/com/campudus/tableaux/database/model/TableauxModel.scala @@ -21,6 +21,7 @@ import scala.util.Try import scala.util.control.NonFatal import java.util.UUID +import org.checkerframework.checker.units.qual.s object TableauxModel { type LinkId = Long @@ -645,6 +646,24 @@ class TableauxModel( } } + def isValueChange[A](column: ColumnType[_], newValue: A, oldValue: A): Boolean = { + column match { + case MultiLanguageColumn(c) => { + println(s"multilang oldValue: $oldValue, newValue: $newValue") + newValue != oldValue + } + case s: SimpleValueColumn[_] => newValue != oldValue + case l: LinkColumn => { + println(s"link oldValue: $oldValue, newValue: $newValue") + newValue != oldValue + } + case a: AttachmentColumn => { + println(s"attachment oldValue: $oldValue, newValue: $newValue") + newValue != oldValue + } + } + } + private def updateOrReplaceValue[A]( table: Table, columnId: ColumnId, @@ -653,6 +672,28 @@ class TableauxModel( replace: Boolean = false, maybeTransaction: Option[DbTransaction] = None )(implicit user: TableauxUser): Future[Cell[_]] = { + + def doUpdateOrReplaceValue(column: ColumnType[_]) = { + for { + _ <- createHistoryModel.createCellsInit(table, rowId, Seq((column, value))) + _ <- + if (replace) { + for { + _ <- createHistoryModel.clearBackLinksWhichWillBeDeleted(table, rowId, Seq((column, value))) + _ <- updateRowModel.clearRowWithValues(table, rowId, Seq((column, value)), deleteRow) + } yield () + } else { + Future.successful(()) + } + + _ <- updateRowModel.updateRow(table, rowId, Seq((column, value)), maybeTransaction) + _ <- invalidateCellAndDependentColumns(column, rowId) + _ <- createHistoryModel.createCells(table, rowId, Seq((column, value))) + + changedCell <- retrieveCell(column, rowId, true) + } yield (changedCell) + } + for { _ <- checkForSettingsTable(table, columnId, "can't update key cell of a settings table") @@ -673,23 +714,16 @@ class TableauxModel( roleModel.checkAuthorization(EditCellValue, ComparisonObjects(table, column, value)) } - _ <- createHistoryModel.createCellsInit(table, rowId, Seq((column, value))) + cell <- retrieveCell(column, rowId, true) + shouldChangeValue = isValueChange(column, value, cell.value) - _ <- - if (replace) { - for { - _ <- createHistoryModel.clearBackLinksWhichWillBeDeleted(table, rowId, Seq((column, value))) - _ <- updateRowModel.clearRowWithValues(table, rowId, Seq((column, value)), deleteRow) - } yield () - } else { - Future.successful(()) + changedCell <- shouldChangeValue match { + case true => doUpdateOrReplaceValue(column) + case false => { + logger.info(s"Value did not change, skipping update for ${table.id} $columnId $rowId") + Future.successful(cell) } - - _ <- updateRowModel.updateRow(table, rowId, Seq((column, value)), maybeTransaction) - _ <- invalidateCellAndDependentColumns(column, rowId) - _ <- createHistoryModel.createCells(table, rowId, Seq((column, value))) - - changedCell <- retrieveCell(column, rowId, true) + } } yield changedCell } diff --git a/src/test/scala/com/campudus/tableaux/api/content/CreateHistoryChangeDetectionTest.scala b/src/test/scala/com/campudus/tableaux/api/content/CreateHistoryChangeDetectionTest.scala index 4916c7b6..30022fcb 100644 --- a/src/test/scala/com/campudus/tableaux/api/content/CreateHistoryChangeDetectionTest.scala +++ b/src/test/scala/com/campudus/tableaux/api/content/CreateHistoryChangeDetectionTest.scala @@ -18,31 +18,6 @@ import org.junit.Assert._ import org.junit.runner.RunWith import org.skyscreamer.jsonassert.{JSONAssert, JSONCompareMode} -// trait TestHelper2 extends MediaTestBase { - -// def toRowsArray(obj: JsonObject): JsonArray = { -// obj.getJsonArray("rows") -// } - -// def getLinksValue(arr: JsonArray, pos: Int): JsonArray = { -// arr.getJsonObject(pos).getJsonArray("value") -// } - -// protected def createTestAttachment(name: String)(implicit c: TestContext): Future[String] = { - -// val path = s"/com/campudus/tableaux/uploads/Screen.Shot.png" -// val mimetype = "application/png" - -// val file = Json.obj("title" -> Json.obj("de-DE" -> name)) - -// for { -// fileUuid <- createFile("de-DE", path, mimetype, None) map (_.getString("uuid")) -// _ <- sendRequest("PUT", s"/files/$fileUuid", file) - -// } yield fileUuid -// } -// } - @RunWith(classOf[VertxUnitRunner]) class CreateHistoryChangeDetectionTest extends TableauxTestBase with TestHelper { @@ -72,8 +47,8 @@ class CreateHistoryChangeDetectionTest extends TableauxTestBase with TestHelper for { _ <- createTableWithMultilanguageColumns("history test") _ <- sendRequest("POST", "/tables/1/rows") - _ <- sendRequest("POST", "/tables/1/columns/1/rows/1", newValue) - _ <- sendRequest("POST", "/tables/1/columns/1/rows/1", newValue) + _ <- sendRequest("PATCH", "/tables/1/columns/1/rows/1", newValue) + _ <- sendRequest("PATCH", "/tables/1/columns/1/rows/1", newValue) rows <- sendRequest("GET", "/tables/1/columns/1/rows/1/history?historyType=cell").map(toRowsArray) } yield { assertEquals(1, rows.size()) @@ -89,14 +64,10 @@ class CreateHistoryChangeDetectionTest extends TableauxTestBase with TestHelper for { _ <- createTableWithMultilanguageColumns("history test") _ <- sendRequest("POST", "/tables/1/rows") - _ <- - sendRequest( - "POST", - "/tables/1/columns/2/rows/1", - newValue - ) // Booleans always gets a initial history entry on first change, so +1 history row - _ <- sendRequest("POST", "/tables/1/columns/2/rows/1", newValue) - rows <- sendRequest("GET", "/tables/1/columns/1/rows/1/history?historyType=cell").map(toRowsArray) + // Booleans always gets a initial history entry on first change, so +1 history row + _ <- sendRequest("PATCH", "/tables/1/columns/2/rows/1", newValue) + _ <- sendRequest("PATCH", "/tables/1/columns/2/rows/1", newValue) + rows <- sendRequest("GET", "/tables/1/columns/2/rows/1/history?historyType=cell").map(toRowsArray) } yield { assertEquals(2, rows.size()) } @@ -111,9 +82,9 @@ class CreateHistoryChangeDetectionTest extends TableauxTestBase with TestHelper for { _ <- createTableWithMultilanguageColumns("history test") _ <- sendRequest("POST", "/tables/1/rows") - _ <- sendRequest("POST", "/tables/1/columns/3/rows/1", newValue) - _ <- sendRequest("POST", "/tables/1/columns/3/rows/1", newValue) - rows <- sendRequest("GET", "/tables/1/columns/1/rows/1/history?historyType=cell").map(toRowsArray) + _ <- sendRequest("PATCH", "/tables/1/columns/3/rows/1", newValue) + _ <- sendRequest("PATCH", "/tables/1/columns/3/rows/1", newValue) + rows <- sendRequest("GET", "/tables/1/columns/3/rows/1/history?historyType=cell").map(toRowsArray) } yield { assertEquals(1, rows.size()) } @@ -128,9 +99,9 @@ class CreateHistoryChangeDetectionTest extends TableauxTestBase with TestHelper for { _ <- createTableWithMultilanguageColumns("history test") _ <- sendRequest("POST", "/tables/1/rows") - _ <- sendRequest("POST", "/tables/1/columns/3/rows/1", newValue) - _ <- sendRequest("POST", "/tables/1/columns/3/rows/1", newValue) - rows <- sendRequest("GET", "/tables/1/columns/1/rows/1/history?historyType=cell").map(toRowsArray) + _ <- sendRequest("PATCH", "/tables/1/columns/7/rows/1", newValue) + _ <- sendRequest("PATCH", "/tables/1/columns/7/rows/1", newValue) + rows <- sendRequest("GET", "/tables/1/columns/7/rows/1/history?historyType=cell").map(toRowsArray) } yield { assertEquals(1, rows.size()) } @@ -146,11 +117,11 @@ class CreateHistoryChangeDetectionTest extends TableauxTestBase with TestHelper for { _ <- createSimpleTableWithCell("table1", multiCountryCurrencyColumn) _ <- sendRequest("POST", "/tables/1/rows") - _ <- sendRequest("POST", "/tables/1/columns/3/rows/1", newValue) - _ <- sendRequest("POST", "/tables/1/columns/3/rows/1", newValue) + _ <- sendRequest("PATCH", "/tables/1/columns/1/rows/1", newValue) + _ <- sendRequest("PATCH", "/tables/1/columns/1/rows/1", newValue) rows <- sendRequest("GET", "/tables/1/columns/1/rows/1/history?historyType=cell").map(toRowsArray) } yield { - assertEquals(2, rows.size()) + assertEquals(1, rows.size()) } } } From 985187f4addb258bed6e06cc2b9342900e2280c1 Mon Sep 17 00:00:00 2001 From: Manfred Zingl Date: Tue, 24 Oct 2023 12:20:24 +0200 Subject: [PATCH 4/9] refactor HistoryModel, add test --- .../database/model/HistoryModel.scala | 9 +++-- .../CreateHistoryChangeDetectionTest.scala | 36 +++++++++---------- 2 files changed, 22 insertions(+), 23 deletions(-) diff --git a/src/main/scala/com/campudus/tableaux/database/model/HistoryModel.scala b/src/main/scala/com/campudus/tableaux/database/model/HistoryModel.scala index 0d241579..a42a59dd 100644 --- a/src/main/scala/com/campudus/tableaux/database/model/HistoryModel.scala +++ b/src/main/scala/com/campudus/tableaux/database/model/HistoryModel.scala @@ -906,19 +906,18 @@ case class CreateHistoryModel(tableauxModel: TableauxModel, connection: Database def createCells(table: Table, rowId: RowId, values: Seq[(ColumnType[_], _)])( implicit user: TableauxUser ): Future[Unit] = { - val columns = values.map({ case (col: ColumnType[_], _) => col }) - val (_, _, _, attachments) = ColumnType.splitIntoTypes(columns) - ColumnType.splitIntoTypesWithValues(values) match { case Failure(ex) => Future.failed(ex) - case Success((simples, multis, links, _)) => + case Success((simples, multis, links, attachments)) => for { _ <- if (simples.isEmpty) Future.successful(()) else createSimple(table, rowId, simples) _ <- if (multis.isEmpty) Future.successful(()) else createTranslation(table, rowId, multis) _ <- if (links.isEmpty) Future.successful(()) else createLinks(table, rowId, links, allowRecursion = true) - _ <- if (attachments.isEmpty) Future.successful(()) else createAttachments(table, rowId, attachments) + _ <- + if (attachments.isEmpty) Future.successful(()) + else createAttachments(table, rowId, attachments.map({ case (column, _) => column })) } yield () } } diff --git a/src/test/scala/com/campudus/tableaux/api/content/CreateHistoryChangeDetectionTest.scala b/src/test/scala/com/campudus/tableaux/api/content/CreateHistoryChangeDetectionTest.scala index 30022fcb..9f4cab38 100644 --- a/src/test/scala/com/campudus/tableaux/api/content/CreateHistoryChangeDetectionTest.scala +++ b/src/test/scala/com/campudus/tableaux/api/content/CreateHistoryChangeDetectionTest.scala @@ -127,24 +127,24 @@ class CreateHistoryChangeDetectionTest extends TableauxTestBase with TestHelper } } -// @RunWith(classOf[VertxUnitRunner]) -// class CreateHistoryChangeLinkDetectionTest extends LinkTestBase with TestHelper { - -// @Test -// def changeLinkValueTwice_xxx(implicit c: TestContext): Unit = { -// okTest { -// val twoLinks = Json.obj("value" -> Json.obj("values" -> Json.arr(1, 2))) - -// for { -// linkColumnId <- setupTwoTablesWithEmptyLinks() +@RunWith(classOf[VertxUnitRunner]) +class CreateHistoryChangeLinkDetectionTest extends LinkTestBase with TestHelper { -// _ <- sendRequest("PUT", s"/tables/1/columns/$linkColumnId/rows/1", twoLinks) -// rows <- sendRequest("GET", "/tables/1/columns/3/rows/1/history?historyType=cell").map(toRowsArray) -// } yield { -// assertEquals(2, rows.size()) -// } -// } -// } + @Test + def changeLinkValueTwice_singleLanguage(implicit c: TestContext): Unit = okTest { + val twoLinks = Json.obj("value" -> Json.obj("values" -> Json.arr(1, 2))) + + for { + linkColumnId <- setupTwoTablesWithEmptyLinks() + + _ = println(s"linkColumnId: $linkColumnId") + _ <- sendRequest("PUT", s"/tables/1/columns/$linkColumnId/rows/1", twoLinks) + _ <- sendRequest("PUT", s"/tables/1/columns/3/rows/1", twoLinks) + rows <- sendRequest("GET", s"/tables/1/columns/3/rows/1/history?historyType=cell").map(toRowsArray) + } yield { + assertEquals(1, rows.size()) + } + } // @Test // def changeLink_singleLanguageMultiIdentifiers(implicit c: TestContext): Unit = { @@ -1610,4 +1610,4 @@ class CreateHistoryChangeDetectionTest extends TableauxTestBase with TestHelper // } // } -// } +} From 0b18dd0e9e5ca55397f883abde4227eabe57c9d3 Mon Sep 17 00:00:00 2001 From: Manfred Zingl Date: Tue, 24 Oct 2023 14:52:06 +0200 Subject: [PATCH 5/9] base tests are working --- .../controller/TableauxController.scala | 3 +- .../database/model/HistoryModel.scala | 89 ++++++++------- .../database/model/TableauxModel.scala | 101 ++++++++---------- 3 files changed, 91 insertions(+), 102 deletions(-) diff --git a/src/main/scala/com/campudus/tableaux/controller/TableauxController.scala b/src/main/scala/com/campudus/tableaux/controller/TableauxController.scala index e5eef03a..4f59bd7a 100644 --- a/src/main/scala/com/campudus/tableaux/controller/TableauxController.scala +++ b/src/main/scala/com/campudus/tableaux/controller/TableauxController.scala @@ -506,7 +506,8 @@ class TableauxController( _ <- roleModel.checkAuthorization(EditCellValue, ComparisonObjects(table, column)) - _ <- repository.createHistoryModel.createCellsInit(table, rowId, Seq((column, uuid))) + oldCell <- repository.retrieveCell(table, column.id, rowId, true) + _ <- repository.createHistoryModel.createCellsInit(table, rowId, oldCell, Seq((column, uuid))) _ <- repository.attachmentModel.delete(Attachment(tableId, columnId, rowId, UUID.fromString(uuid), None)) _ <- CacheClient(this).invalidateCellValue(tableId, columnId, rowId) _ <- repository.createHistoryModel.createCells(table, rowId, Seq((column, uuid))) diff --git a/src/main/scala/com/campudus/tableaux/database/model/HistoryModel.scala b/src/main/scala/com/campudus/tableaux/database/model/HistoryModel.scala index a42a59dd..2c11cdd8 100644 --- a/src/main/scala/com/campudus/tableaux/database/model/HistoryModel.scala +++ b/src/main/scala/com/campudus/tableaux/database/model/HistoryModel.scala @@ -514,7 +514,7 @@ case class CreateHistoryModel(tableauxModel: TableauxModel, connection: Database * If we didn't call this method on any cell change/deletion the previously valid value wouldn't be logged in a * history table. */ - def createCellsInit(table: Table, rowId: RowId, values: Seq[(ColumnType[_], _)])( + def createCellsInit(table: Table, rowId: RowId, oldCell: Cell[_], values: Seq[(ColumnType[_], _)])( implicit user: TableauxUser ): Future[Unit] = { val columns = values.map({ case (col: ColumnType[_], _) => col }) @@ -532,10 +532,13 @@ case class CreateHistoryModel(tableauxModel: TableauxModel, connection: Database } yield (langtagSeq, column) for { - _ <- if (simples.isEmpty) Future.successful(()) else createSimpleInit(table, rowId, simples) - _ <- if (multis.isEmpty) Future.successful(()) else createTranslationInit(table, rowId, langtagColumns) - _ <- if (links.isEmpty) Future.successful(()) else createLinksInit(table, rowId, links) - _ <- if (attachments.isEmpty) Future.successful(()) else createAttachmentsInit(table, rowId, attachments) + _ <- if (simples.isEmpty) Future.successful(()) else createSimpleInit(table, rowId, oldCell, simples) + _ <- + if (multis.isEmpty) Future.successful(()) else createTranslationInit(table, rowId, oldCell, langtagColumns) + _ <- if (links.isEmpty) Future.successful(()) else createLinksInit(table, rowId, oldCell, links) + _ <- + if (attachments.isEmpty) Future.successful(()) + else createAttachmentsInit(table, rowId, oldCell, attachments) } yield () } } @@ -546,7 +549,7 @@ case class CreateHistoryModel(tableauxModel: TableauxModel, connection: Database * If we didn't call this method on any cell change/deletion the previously valid value wouldn't be logged in a * history table. */ - def createClearCellInit(table: Table, rowId: RowId, columns: Seq[ColumnType[_]])( + def createClearCellInit(table: Table, rowId: RowId, oldCell: Cell[_], columns: Seq[ColumnType[_]])( implicit user: TableauxUser ): Future[Unit] = { val (simples, multis, links, attachments) = ColumnType.splitIntoTypes(columns) @@ -558,14 +561,14 @@ case class CreateHistoryModel(tableauxModel: TableauxModel, connection: Database } yield (langtag, column) for { - _ <- if (simples.isEmpty) Future.successful(()) else createSimpleInit(table, rowId, simples) - _ <- if (multis.isEmpty) Future.successful(()) else createTranslationInit(table, rowId, langtagColumns) - _ <- if (links.isEmpty) Future.successful(()) else createLinksInit(table, rowId, links) - _ <- if (attachments.isEmpty) Future.successful(()) else createAttachmentsInit(table, rowId, attachments) + _ <- if (simples.isEmpty) Future.successful(()) else createSimpleInit(table, rowId, oldCell, simples) + _ <- if (multis.isEmpty) Future.successful(()) else createTranslationInit(table, rowId, oldCell, langtagColumns) + _ <- if (links.isEmpty) Future.successful(()) else createLinksInit(table, rowId, oldCell, links) + _ <- if (attachments.isEmpty) Future.successful(()) else createAttachmentsInit(table, rowId, oldCell, attachments) } yield () } - def createAttachmentsInit(table: Table, rowId: RowId, columns: Seq[AttachmentColumn])( + def createAttachmentsInit(table: Table, rowId: RowId, oldCell: Cell[_], columns: Seq[AttachmentColumn])( implicit user: TableauxUser ): Future[Seq[Unit]] = { Future.sequence(columns.map({ column => @@ -588,7 +591,7 @@ case class CreateHistoryModel(tableauxModel: TableauxModel, connection: Database })) } - def createLinksInit(table: Table, rowId: RowId, links: Seq[LinkColumn])( + def createLinksInit(table: Table, rowId: RowId, oldCell: Cell[_], links: Seq[LinkColumn])( implicit user: TableauxUser ): Future[Seq[Unit]] = { @@ -612,18 +615,16 @@ case class CreateHistoryModel(tableauxModel: TableauxModel, connection: Database })) } - private def createSimpleInit(table: Table, rowId: RowId, simples: Seq[SimpleValueColumn[_]])( + private def createSimpleInit(table: Table, rowId: RowId, oldCell: Cell[_], simples: Seq[SimpleValueColumn[_]])( implicit user: TableauxUser ): Future[Seq[Unit]] = { def createIfNotEmpty(column: SimpleValueColumn[_]): Future[Unit] = { - for { - value <- retrieveCellValue(table, column, rowId) - _ <- value match { - case Some(v) => createSimple(table, rowId, Seq((column, Option(v)))) - case None => Future.successful(()) - } - } yield () + val value = toInitCellValueOption(column, oldCell) + value match { + case Some(v) => createSimple(table, rowId, Seq((column, Option(v)))).map(_ => ()) + case None => Future.successful(()) + } } Future.sequence(simples.map({ column => @@ -637,6 +638,7 @@ case class CreateHistoryModel(tableauxModel: TableauxModel, connection: Database private def createTranslationInit( table: Table, rowId: RowId, + oldCell: Cell[_], langtagColumns: Seq[(Seq[String], SimpleValueColumn[_])] )(implicit user: TableauxUser): Future[Seq[Unit]] = { @@ -651,13 +653,11 @@ case class CreateHistoryModel(tableauxModel: TableauxModel, connection: Database .toSeq def createIfNotEmpty(column: SimpleValueColumn[_], langtag: String): Future[Unit] = { - for { - value <- retrieveCellValue(table, column, rowId, Option(langtag)) - _ <- value match { - case Some(v) => createTranslation(table, rowId, Seq((column, Map(langtag -> Option(v))))) - case None => Future.successful(()) - } - } yield () + val value = toInitCellValueOption(column, oldCell, Option(langtag)) + value match { + case Some(v) => createTranslation(table, rowId, Seq((column, Map(langtag -> Option(v))))).map(_ => ()) + case None => Future.successful(()) + } } Future.sequence( @@ -691,28 +691,23 @@ case class CreateHistoryModel(tableauxModel: TableauxModel, connection: Database ) } - private def retrieveCellValue( - table: Table, + private def toInitCellValueOption( column: ColumnType[_], - rowId: RowId, + cell: Cell[_], langtagCountryOpt: Option[String] = None - )(implicit user: TableauxUser): Future[Option[Any]] = { - for { - cell <- tableauxModel.retrieveCell(table, column.id, rowId, isInternalCall = true) - } yield { - Option(cell.value) match { - case Some(v) => - column match { - case MultiLanguageColumn(_) => - val rawValue = cell.getJson.getJsonObject("value") - column match { - case _: BooleanColumn => Option(rawValue.getBoolean(langtagCountryOpt.getOrElse(""), false)) - case _ => Option(rawValue.getValue(langtagCountryOpt.getOrElse(""))) - } - case _: SimpleValueColumn[_] => Some(v) - } - case _ => None - } + ): Option[Any] = { + Option(cell.value) match { + case Some(v) => + column match { + case MultiLanguageColumn(_) => + val rawValue = cell.getJson.getJsonObject("value") + column match { + case _: BooleanColumn => Option(rawValue.getBoolean(langtagCountryOpt.getOrElse(""), false)) + case _ => Option(rawValue.getValue(langtagCountryOpt.getOrElse(""))) + } + case _: SimpleValueColumn[_] => Some(v) + } + case _ => None } } diff --git a/src/main/scala/com/campudus/tableaux/database/model/TableauxModel.scala b/src/main/scala/com/campudus/tableaux/database/model/TableauxModel.scala index e2ad66e8..fd572210 100644 --- a/src/main/scala/com/campudus/tableaux/database/model/TableauxModel.scala +++ b/src/main/scala/com/campudus/tableaux/database/model/TableauxModel.scala @@ -21,7 +21,6 @@ import scala.util.Try import scala.util.control.NonFatal import java.util.UUID -import org.checkerframework.checker.units.qual.s object TableauxModel { type LinkId = Long @@ -307,7 +306,16 @@ class TableauxModel( .collect({ case c: LinkColumn if !c.linkDirection.constraint.deleteCascade => c }) .map(col => (col, 0)) - _ <- createHistoryModel.createCellsInit(table, rowId, columnValueLinks) + _ <- Future.sequence( + columns.map({ + case c: ColumnType[_] => { + for { + cell <- retrieveCell(c, rowId, true) + _ <- createHistoryModel.createCellsInit(table, rowId, cell, Seq.empty) + } yield () + } + }) + ) linkList <- retrieveDependentCells(table, rowId) @@ -532,7 +540,8 @@ class TableauxModel( _ <- column match { case linkColumn: LinkColumn => { for { - _ <- createHistoryModel.createCellsInit(table, rowId, Seq((column, Seq(toId)))) + oldCell <- retrieveCell(column, rowId, true) + _ <- createHistoryModel.createCellsInit(table, rowId, oldCell, Seq((column, Seq(toId)))) _ <- updateRowModel.deleteLink(table, linkColumn, rowId, toId, deleteRow) _ <- invalidateCellAndDependentColumns(column, rowId) _ <- createHistoryModel.deleteLink(table, linkColumn, rowId, toId) @@ -559,7 +568,8 @@ class TableauxModel( _ <- column match { case linkColumn: LinkColumn => { for { - _ <- createHistoryModel.createCellsInit(table, rowId, Seq((linkColumn, Seq(rowId)))) + oldCell <- retrieveCell(column, rowId, true) + _ <- createHistoryModel.createCellsInit(table, rowId, oldCell, Seq((linkColumn, Seq(rowId)))) _ <- updateRowModel.updateLinkOrder(table, linkColumn, rowId, toId, locationType) _ <- invalidateCellAndDependentColumns(column, rowId) _ <- createHistoryModel.updateLinks(table, linkColumn, Seq(rowId)) @@ -646,24 +656,6 @@ class TableauxModel( } } - def isValueChange[A](column: ColumnType[_], newValue: A, oldValue: A): Boolean = { - column match { - case MultiLanguageColumn(c) => { - println(s"multilang oldValue: $oldValue, newValue: $newValue") - newValue != oldValue - } - case s: SimpleValueColumn[_] => newValue != oldValue - case l: LinkColumn => { - println(s"link oldValue: $oldValue, newValue: $newValue") - newValue != oldValue - } - case a: AttachmentColumn => { - println(s"attachment oldValue: $oldValue, newValue: $newValue") - newValue != oldValue - } - } - } - private def updateOrReplaceValue[A]( table: Table, columnId: ColumnId, @@ -672,28 +664,6 @@ class TableauxModel( replace: Boolean = false, maybeTransaction: Option[DbTransaction] = None )(implicit user: TableauxUser): Future[Cell[_]] = { - - def doUpdateOrReplaceValue(column: ColumnType[_]) = { - for { - _ <- createHistoryModel.createCellsInit(table, rowId, Seq((column, value))) - _ <- - if (replace) { - for { - _ <- createHistoryModel.clearBackLinksWhichWillBeDeleted(table, rowId, Seq((column, value))) - _ <- updateRowModel.clearRowWithValues(table, rowId, Seq((column, value)), deleteRow) - } yield () - } else { - Future.successful(()) - } - - _ <- updateRowModel.updateRow(table, rowId, Seq((column, value)), maybeTransaction) - _ <- invalidateCellAndDependentColumns(column, rowId) - _ <- createHistoryModel.createCells(table, rowId, Seq((column, value))) - - changedCell <- retrieveCell(column, rowId, true) - } yield (changedCell) - } - for { _ <- checkForSettingsTable(table, columnId, "can't update key cell of a settings table") @@ -714,17 +684,39 @@ class TableauxModel( roleModel.checkAuthorization(EditCellValue, ComparisonObjects(table, column, value)) } - cell <- retrieveCell(column, rowId, true) - shouldChangeValue = isValueChange(column, value, cell.value) + oldCell <- retrieveCell(column, rowId, true) - changedCell <- shouldChangeValue match { - case true => doUpdateOrReplaceValue(column) - case false => { - logger.info(s"Value did not change, skipping update for ${table.id} $columnId $rowId") - Future.successful(cell) + // _ <- createHistoryModel.createCellsInit(table, rowId, Seq((column, value))) + + _ <- + if (replace) { + for { + _ <- createHistoryModel.clearBackLinksWhichWillBeDeleted(table, rowId, Seq((column, value))) + _ <- updateRowModel.clearRowWithValues(table, rowId, Seq((column, value)), deleteRow) + } yield () + } else { + Future.successful(()) } - } - } yield changedCell + + _ <- updateRowModel.updateRow(table, rowId, Seq((column, value)), maybeTransaction) + _ <- invalidateCellAndDependentColumns(column, rowId) + newCell <- retrieveCell(column, rowId, true) + + _ = println(s"value changed from ${oldCell.value} to ${newCell.value} isEqual ${oldCell.value == newCell.value}") + _ <- + if (oldCell != newCell) { + println("createHistoryModel.createCellsInit") + for { + _ <- createHistoryModel.createCellsInit(table, rowId, oldCell, Seq((column, value))) + _ = println("createHistoryModel.createCells") + _ <- createHistoryModel.createCells(table, rowId, Seq((column, value))) + } yield () + } else { + Future.successful(()) + } + + // changedCell <- retrieveCell(column, rowId, true) + } yield newCell } def updateCellValue[A](table: Table, columnId: ColumnId, rowId: RowId, value: A)( @@ -753,7 +745,8 @@ class TableauxModel( roleModel.checkAuthorization(EditCellValue, ComparisonObjects(table, column)) } - _ <- createHistoryModel.createClearCellInit(table, rowId, Seq(column)) + oldCell <- retrieveCell(column, rowId, true) + _ <- createHistoryModel.createClearCellInit(table, rowId, oldCell, Seq(column)) _ <- createHistoryModel.createClearCell(table, rowId, Seq(column)) _ <- updateRowModel.clearRow(table, rowId, Seq(column), deleteRow) _ <- invalidateCellAndDependentColumns(column, rowId) From 481a69f2312890a71785adde53b5672b97fa44b0 Mon Sep 17 00:00:00 2001 From: Manfred Zingl Date: Tue, 24 Oct 2023 15:41:40 +0200 Subject: [PATCH 6/9] refactorings --- .../database/model/HistoryModel.scala | 71 +- .../database/model/TableauxModel.scala | 13 +- .../CreateHistoryChangeDetectionTest.scala | 1509 +---------------- 3 files changed, 90 insertions(+), 1503 deletions(-) diff --git a/src/main/scala/com/campudus/tableaux/database/model/HistoryModel.scala b/src/main/scala/com/campudus/tableaux/database/model/HistoryModel.scala index 2c11cdd8..a18b694f 100644 --- a/src/main/scala/com/campudus/tableaux/database/model/HistoryModel.scala +++ b/src/main/scala/com/campudus/tableaux/database/model/HistoryModel.scala @@ -141,18 +141,42 @@ case class CreateHistoryModel(tableauxModel: TableauxModel, connection: Database val tableModel = new TableModel(connection) val attachmentModel = AttachmentModel(connection) + private def toInitCellValueOption( + column: ColumnType[_], + cell: Cell[_], + langtagCountryOpt: Option[String] = None + ): Option[Any] = { + Option(cell.value) match { + case Some(v) => + column match { + case MultiLanguageColumn(_) => + val rawValue = cell.getJson.getJsonObject("value") + column match { + case _: BooleanColumn => Option(rawValue.getBoolean(langtagCountryOpt.getOrElse(""), false)) + case _ => Option(rawValue.getValue(langtagCountryOpt.getOrElse(""))) + } + case _: SimpleValueColumn[_] => Some(v) + } + case _ => None + } + } + + private def toInitLinkIds(column: LinkColumn, cell: Cell[_]): Seq[RowId] = { + cell.getJson + .getJsonArray("value") + .asScala + .map(_.asInstanceOf[JsonObject]) + .map(_.getLong("id").longValue()) + .toSeq + } + private def retrieveCurrentLinkIds(table: Table, column: LinkColumn, rowId: RowId)( implicit user: TableauxUser ): Future[Seq[RowId]] = { for { cell <- tableauxModel.retrieveCell(table, column.id, rowId, isInternalCall = true) } yield { - cell.getJson - .getJsonArray("value") - .asScala - .map(_.asInstanceOf[JsonObject]) - .map(_.getLong("id").longValue()) - .toSeq + toInitLinkIds(column, cell) } } @@ -596,15 +620,12 @@ case class CreateHistoryModel(tableauxModel: TableauxModel, connection: Database ): Future[Seq[Unit]] = { def createIfNotEmpty(linkColumn: LinkColumn): Future[Unit] = { - for { - linkIds <- retrieveCurrentLinkIds(table, linkColumn, rowId) - _ <- - if (linkIds.nonEmpty) { - createLinks(table, rowId, Seq((linkColumn, linkIds)), allowRecursion = true) - } else { - Future.successful(()) - } - } yield () + val linkIds = toInitLinkIds(linkColumn, oldCell) + if (linkIds.nonEmpty) { + createLinks(table, rowId, Seq((linkColumn, linkIds)), allowRecursion = true).map(_ => ()) + } else { + Future.successful(()) + } } Future.sequence(links.map({ linkColumn => @@ -691,26 +712,6 @@ case class CreateHistoryModel(tableauxModel: TableauxModel, connection: Database ) } - private def toInitCellValueOption( - column: ColumnType[_], - cell: Cell[_], - langtagCountryOpt: Option[String] = None - ): Option[Any] = { - Option(cell.value) match { - case Some(v) => - column match { - case MultiLanguageColumn(_) => - val rawValue = cell.getJson.getJsonObject("value") - column match { - case _: BooleanColumn => Option(rawValue.getBoolean(langtagCountryOpt.getOrElse(""), false)) - case _ => Option(rawValue.getValue(langtagCountryOpt.getOrElse(""))) - } - case _: SimpleValueColumn[_] => Some(v) - } - case _ => None - } - } - /** * Writes empty back link history for the linked rows that are going to be deleted. The difference of the existing * linked foreign row IDs and the linked foreign row IDs after the change are going to be cleared. diff --git a/src/main/scala/com/campudus/tableaux/database/model/TableauxModel.scala b/src/main/scala/com/campudus/tableaux/database/model/TableauxModel.scala index fd572210..e7021a30 100644 --- a/src/main/scala/com/campudus/tableaux/database/model/TableauxModel.scala +++ b/src/main/scala/com/campudus/tableaux/database/model/TableauxModel.scala @@ -656,6 +656,10 @@ class TableauxModel( } } + private def hasCellChanged(oldCell: Cell[_], newCell: Cell[_]): Boolean = { + oldCell.value != newCell.value + } + private def updateOrReplaceValue[A]( table: Table, columnId: ColumnId, @@ -686,8 +690,6 @@ class TableauxModel( oldCell <- retrieveCell(column, rowId, true) - // _ <- createHistoryModel.createCellsInit(table, rowId, Seq((column, value))) - _ <- if (replace) { for { @@ -702,20 +704,15 @@ class TableauxModel( _ <- invalidateCellAndDependentColumns(column, rowId) newCell <- retrieveCell(column, rowId, true) - _ = println(s"value changed from ${oldCell.value} to ${newCell.value} isEqual ${oldCell.value == newCell.value}") _ <- - if (oldCell != newCell) { - println("createHistoryModel.createCellsInit") + if (hasCellChanged(oldCell, newCell)) { for { _ <- createHistoryModel.createCellsInit(table, rowId, oldCell, Seq((column, value))) - _ = println("createHistoryModel.createCells") _ <- createHistoryModel.createCells(table, rowId, Seq((column, value))) } yield () } else { Future.successful(()) } - - // changedCell <- retrieveCell(column, rowId, true) } yield newCell } diff --git a/src/test/scala/com/campudus/tableaux/api/content/CreateHistoryChangeDetectionTest.scala b/src/test/scala/com/campudus/tableaux/api/content/CreateHistoryChangeDetectionTest.scala index 9f4cab38..0fa2e31e 100644 --- a/src/test/scala/com/campudus/tableaux/api/content/CreateHistoryChangeDetectionTest.scala +++ b/src/test/scala/com/campudus/tableaux/api/content/CreateHistoryChangeDetectionTest.scala @@ -19,7 +19,7 @@ import org.junit.runner.RunWith import org.skyscreamer.jsonassert.{JSONAssert, JSONCompareMode} @RunWith(classOf[VertxUnitRunner]) -class CreateHistoryChangeDetectionTest extends TableauxTestBase with TestHelper { +class CreateHistoryChangeDetectionTest extends LinkTestBase with TestHelper { @Test def changeSimpleValueTwice_changeACellOnlyOnce(implicit c: TestContext): Unit = { @@ -125,10 +125,6 @@ class CreateHistoryChangeDetectionTest extends TableauxTestBase with TestHelper } } } -} - -@RunWith(classOf[VertxUnitRunner]) -class CreateHistoryChangeLinkDetectionTest extends LinkTestBase with TestHelper { @Test def changeLinkValueTwice_singleLanguage(implicit c: TestContext): Unit = okTest { @@ -139,1475 +135,68 @@ class CreateHistoryChangeLinkDetectionTest extends LinkTestBase with TestHelper _ = println(s"linkColumnId: $linkColumnId") _ <- sendRequest("PUT", s"/tables/1/columns/$linkColumnId/rows/1", twoLinks) - _ <- sendRequest("PUT", s"/tables/1/columns/3/rows/1", twoLinks) - rows <- sendRequest("GET", s"/tables/1/columns/3/rows/1/history?historyType=cell").map(toRowsArray) + _ <- sendRequest("PUT", s"/tables/1/columns/$linkColumnId/rows/1", twoLinks) + rows <- sendRequest("GET", s"/tables/1/columns/$linkColumnId/rows/1/history?historyType=cell").map(toRowsArray) } yield { assertEquals(1, rows.size()) } } -// @Test -// def changeLink_singleLanguageMultiIdentifiers(implicit c: TestContext): Unit = { -// okTest { -// val twoLinks = Json.obj("value" -> Json.obj("values" -> Json.arr(1, 2))) - -// for { -// linkColumnId <- setupTwoTablesWithEmptyLinks() - -// // Change target table structure to have a second identifier -// _ <- sendRequest("POST", s"/tables/2/columns/2", Json.obj("identifier" -> true)) - -// _ <- sendRequest("PUT", s"/tables/1/columns/$linkColumnId/rows/1", twoLinks) -// rows <- sendRequest("GET", "/tables/1/columns/3/rows/1/history?historyType=cell").map(toRowsArray) -// } yield { -// assertEquals(2, rows.size()) -// } -// } -// } -// } - -// @RunWith(classOf[VertxUnitRunner]) -// class CreateBidirectionalLinkHistoryTest extends LinkTestBase with TestHelper { - -// @Test -// def changeLink_addOneLink(implicit c: TestContext): Unit = { -// okTest { -// val putLink = Json.obj("value" -> Json.obj("values" -> Json.arr(5))) - -// val linkTable = """[ {"id": 5, "value": "table2RowId3"} ]""".stripMargin -// val targetLinkTable = """[ {"id": 1, "value": "table1row1"} ]""".stripMargin - -// for { -// _ <- setupTwoTablesWithEmptyLinks() - -// _ <- sendRequest("PUT", s"/tables/1/columns/3/rows/1", putLink) - -// history <- sendRequest("GET", "/tables/1/columns/3/rows/1/history?historyType=cell").map(toRowsArray) -// targetHistory <- sendRequest("GET", "/tables/2/columns/3/rows/5/history?historyType=cell").map(toRowsArray) -// historyLinks = getLinksValue(history, 0) -// historyTargetLinks = getLinksValue(targetHistory, 0) -// } yield { -// JSONAssert.assertEquals(linkTable, historyLinks.toString, JSONCompareMode.LENIENT) -// JSONAssert.assertEquals(targetLinkTable, historyTargetLinks.toString, JSONCompareMode.LENIENT) -// } -// } -// } - -// @Test -// def changeLink_addTwoLinksAtOnce(implicit c: TestContext): Unit = { -// okTest { -// val putLink = Json.obj("value" -> Json.obj("values" -> Json.arr(4, 5))) - -// val linkTable = """[ {"id": 4, "value": "table2RowId2"}, {"id": 5, "value": "table2RowId3"} ]""".stripMargin -// val targetLinkTable1 = """[ {"id": 1, "value": "table1row1"} ]""".stripMargin -// val targetLinkTable2 = """[ {"id": 1, "value": "table1row1"} ]""".stripMargin - -// for { -// _ <- setupTwoTablesWithEmptyLinks() - -// _ <- sendRequest("PUT", s"/tables/1/columns/3/rows/1", putLink) - -// history <- sendRequest("GET", "/tables/1/columns/3/rows/1/history?historyType=cell").map(toRowsArray) -// targetHistory1 <- sendRequest("GET", "/tables/2/columns/3/rows/4/history?historyType=cell").map(toRowsArray) -// targetHistory2 <- sendRequest("GET", "/tables/2/columns/3/rows/5/history?historyType=cell").map(toRowsArray) -// historyLinks = getLinksValue(history, 0) -// historyTargetLinks1 = getLinksValue(targetHistory1, 0) -// historyTargetLinks2 = getLinksValue(targetHistory2, 0) -// } yield { -// JSONAssert.assertEquals(linkTable, historyLinks.toString, JSONCompareMode.STRICT) -// JSONAssert.assertEquals(targetLinkTable1, historyTargetLinks1.toString, JSONCompareMode.LENIENT) -// JSONAssert.assertEquals(targetLinkTable2, historyTargetLinks2.toString, JSONCompareMode.LENIENT) -// } -// } -// } - -// @Test -// def changeLink_addASecondLink(implicit c: TestContext): Unit = { -// okTest { -// val putInitialLink = Json.obj("value" -> Json.obj("values" -> Json.arr(5))) -// val patchSecondLink = Json.obj("value" -> Json.obj("values" -> Json.arr(4))) - -// val linkTable = -// """[ -// | {"id": 5, "value": "table2RowId3"}, -// | {"id": 4, "value": "table2RowId2"} -// |]""".stripMargin - -// val backLink = """[ {"id": 1, "value": "table1row1"} ]""".stripMargin - -// for { -// _ <- setupTwoTablesWithEmptyLinks() - -// _ <- sendRequest("PUT", s"/tables/1/columns/3/rows/1", putInitialLink) -// _ <- sendRequest("PATCH", s"/tables/1/columns/3/rows/1", patchSecondLink) - -// history <- sendRequest("GET", "/tables/1/columns/3/rows/1/history?historyType=cell").map(toRowsArray) -// targetHistory1 <- sendRequest("GET", "/tables/2/columns/3/rows/4/history?historyType=cell").map(toRowsArray) -// targetHistory2 <- sendRequest("GET", "/tables/2/columns/3/rows/5/history?historyType=cell").map(toRowsArray) - -// historyLinks = getLinksValue(history, 1) -// historyTargetLinks1 = getLinksValue(targetHistory1, 0) -// historyTargetLinks2 = getLinksValue(targetHistory2, 0) -// } yield { -// JSONAssert.assertEquals(linkTable, historyLinks.toString, JSONCompareMode.STRICT) -// JSONAssert.assertEquals(backLink, historyTargetLinks1.toString, JSONCompareMode.LENIENT) -// assertEquals(1, targetHistory1.size()) -// JSONAssert.assertEquals(backLink, historyTargetLinks2.toString, JSONCompareMode.LENIENT) -// assertEquals(1, targetHistory2.size()) -// } -// } -// } - -// @Test -// def changeLink_oneLinkExisting_addTwoLinksAtOnce(implicit c: TestContext): Unit = { -// okTest { -// val putInitialLink = Json.obj("value" -> Json.obj("values" -> Json.arr(5))) -// val patchSecondLinks = Json.obj("value" -> Json.obj("values" -> Json.arr(4, 3))) - -// val linkTable = -// """[ -// | {"id": 5, "value": "table2RowId3"}, -// | {"id": 4, "value": "table2RowId2"}, -// | {"id": 3, "value": "table2RowId1"} -// |]""".stripMargin - -// val backLink = """[ {"id": 1, "value": "table1row1"} ]""".stripMargin - -// for { -// _ <- setupTwoTablesWithEmptyLinks() - -// _ <- sendRequest("PUT", s"/tables/1/columns/3/rows/1", putInitialLink) -// _ <- sendRequest("PATCH", s"/tables/1/columns/3/rows/1", patchSecondLinks) - -// history <- sendRequest("GET", "/tables/1/columns/3/rows/1/history?historyType=cell").map(toRowsArray) -// targetHistoryRows1 <- sendRequest("GET", "/tables/2/columns/3/rows/3/history?historyType=cell").map(toRowsArray) -// targetHistoryRows2 <- sendRequest("GET", "/tables/2/columns/3/rows/4/history?historyType=cell").map(toRowsArray) -// targetHistoryRows3 <- sendRequest("GET", "/tables/2/columns/3/rows/5/history?historyType=cell").map(toRowsArray) - -// initialHistoryLinks = history.getJsonObject(0).getJsonArray("value") -// historyLinks = history.getJsonObject(1).getJsonArray("value") -// historyTargetLinks1 = targetHistoryRows1.getJsonObject(0).getJsonArray("value") -// historyTargetLinks2 = targetHistoryRows2.getJsonObject(0).getJsonArray("value") -// historyTargetLinks3 = targetHistoryRows3.getJsonObject(0).getJsonArray("value") -// } yield { -// assertEquals(1, initialHistoryLinks.size()) -// assertEquals(3, historyLinks.size()) -// JSONAssert.assertEquals(linkTable, historyLinks.toString, JSONCompareMode.STRICT) - -// assertEquals(1, targetHistoryRows1.size()) -// assertEquals(1, targetHistoryRows2.size()) -// assertEquals(1, targetHistoryRows3.size()) - -// JSONAssert.assertEquals(backLink, historyTargetLinks1.toString, JSONCompareMode.LENIENT) -// JSONAssert.assertEquals(backLink, historyTargetLinks2.toString, JSONCompareMode.LENIENT) -// JSONAssert.assertEquals(backLink, historyTargetLinks3.toString, JSONCompareMode.LENIENT) -// } -// } -// } - -// @Test -// def changeLink_reverseOrder_doesNotChangeBackLinks(implicit c: TestContext): Unit = { -// okTest { -// val putLinks = -// """ -// |{"value": -// | { "values": [3, 4, 5] } -// |} -// |""".stripMargin - -// val expected = -// """ -// |[ -// | {"id": 5, "value": "table2RowId3"}, -// | {"id": 4, "value": "table2RowId2"}, -// | {"id": 3, "value": "table2RowId1"} -// |] -// """.stripMargin - -// for { -// _ <- setupTwoTablesWithEmptyLinks() - -// _ <- sendRequest("PUT", s"/tables/1/columns/3/rows/1", Json.fromObjectString(putLinks)) - -// _ <- sendRequest("PUT", s"/tables/1/columns/3/rows/1/link/3/order", Json.obj("location" -> "end")) -// _ <- sendRequest("PUT", s"/tables/1/columns/3/rows/1/link/5/order", Json.obj("location" -> "start")) - -// backLinkHistory <- sendRequest("GET", "/tables/2/columns/3/rows/1/history?historyType=cell").map(toRowsArray) - -// rows <- sendRequest("GET", s"/tables/1/columns/3/rows/1/history?historyType=cell").map(toRowsArray) -// historyAfterCreation = getLinksValue(rows, 2) -// } yield { -// JSONAssert.assertEquals(expected, historyAfterCreation.toString, JSONCompareMode.STRICT_ORDER) -// assertEquals(0, backLinkHistory.size()) -// } -// } -// } - -// @Test -// def changeLink_threeExistingLinks_deleteOneLink(implicit c: TestContext): Unit = { -// okTest { -// val putLinks = Json.obj("value" -> Json.obj("values" -> Json.arr(3, 4, 5))) - -// val expected = -// """ -// |[ -// | {"id": 3, "value": "table2RowId1"}, -// | {"id": 5, "value": "table2RowId3"} -// |] -// """.stripMargin - -// for { -// _ <- setupTwoTablesWithEmptyLinks() - -// _ <- sendRequest("PUT", s"/tables/1/columns/3/rows/1", putLinks) -// _ <- sendRequest("DELETE", s"/tables/1/columns/3/rows/1/link/4") - -// rows <- sendRequest("GET", "/tables/1/columns/3/rows/1/history?historyType=cell").map(toRowsArray) -// historyAfterCreation = getLinksValue(rows, 1) -// backLinkRow3 <- sendRequest("GET", "/tables/2/columns/3/rows/3/history?historyType=cell").map(toRowsArray) -// backLinkRow4 <- sendRequest("GET", "/tables/2/columns/3/rows/4/history?historyType=cell").map(toRowsArray) -// backLinkRow5 <- sendRequest("GET", "/tables/2/columns/3/rows/5/history?historyType=cell").map(toRowsArray) - -// } yield { -// JSONAssert.assertEquals(expected, historyAfterCreation.toString, JSONCompareMode.STRICT_ORDER) - -// assertEquals(1, backLinkRow3.size()) -// assertEquals(2, backLinkRow4.size()) -// assertEquals(1, backLinkRow5.size()) - -// JSONAssert.assertEquals("""[{"id": 1}]""", getLinksValue(backLinkRow4, 0).toString, JSONCompareMode.LENIENT) -// JSONAssert.assertEquals("[]", getLinksValue(backLinkRow4, 1).toString, JSONCompareMode.LENIENT) -// } -// } -// } - -// @Test -// def changeLink_threeExistingLinks_deleteCell(implicit c: TestContext): Unit = { -// okTest { -// val putLinks = Json.obj("value" -> Json.obj("values" -> Json.arr(3, 4, 5))) - -// for { -// _ <- setupTwoTablesWithEmptyLinks() - -// _ <- sendRequest("PUT", s"/tables/1/columns/3/rows/1", putLinks) -// _ <- sendRequest("DELETE", s"/tables/1/columns/3/rows/1") - -// rows <- sendRequest("GET", "/tables/1/columns/3/rows/1/history?historyType=cell").map(toRowsArray) -// historyAfterCreation = getLinksValue(rows, 1) -// backLinkRow3 <- sendRequest("GET", "/tables/2/columns/3/rows/3/history?historyType=cell").map(toRowsArray) -// backLinkRow4 <- sendRequest("GET", "/tables/2/columns/3/rows/4/history?historyType=cell").map(toRowsArray) -// backLinkRow5 <- sendRequest("GET", "/tables/2/columns/3/rows/5/history?historyType=cell").map(toRowsArray) - -// } yield { -// JSONAssert.assertEquals("[]", historyAfterCreation.toString, JSONCompareMode.LENIENT) - -// assertEquals(2, backLinkRow3.size()) -// assertEquals(2, backLinkRow4.size()) -// assertEquals(2, backLinkRow5.size()) - -// JSONAssert.assertEquals("[]", getLinksValue(backLinkRow3, 1).toString, JSONCompareMode.LENIENT) -// JSONAssert.assertEquals("[]", getLinksValue(backLinkRow4, 1).toString, JSONCompareMode.LENIENT) -// JSONAssert.assertEquals("[]", getLinksValue(backLinkRow5, 1).toString, JSONCompareMode.LENIENT) -// } -// } -// } - -// @Test -// def changeLink_threeExistingLinks_putOne(implicit c: TestContext): Unit = { -// okTest { -// val putLinks = Json.obj("value" -> Json.obj("values" -> Json.arr(3, 4, 5))) -// val putNewLink = Json.obj("value" -> Json.obj("values" -> Json.arr(4))) - -// for { -// _ <- setupTwoTablesWithEmptyLinks() - -// _ <- sendRequest("PUT", s"/tables/1/columns/3/rows/1", putLinks) -// _ <- sendRequest("PUT", s"/tables/1/columns/3/rows/1", putNewLink) - -// rows <- sendRequest("GET", "/tables/1/columns/3/rows/1/history?historyType=cell").map(toRowsArray) -// historyAfterCreation = getLinksValue(rows, 1) -// backLinkRow3 <- sendRequest("GET", "/tables/2/columns/3/rows/3/history?historyType=cell").map(toRowsArray) -// backLinkRow4 <- sendRequest("GET", "/tables/2/columns/3/rows/4/history?historyType=cell").map(toRowsArray) -// backLinkRow5 <- sendRequest("GET", "/tables/2/columns/3/rows/5/history?historyType=cell").map(toRowsArray) - -// } yield { -// JSONAssert -// .assertEquals( -// """[{"id": 4, "value": "table2RowId2"}]""", -// historyAfterCreation.toString, -// JSONCompareMode.STRICT -// ) - -// assertEquals(2, backLinkRow3.size()) -// assertEquals(2, backLinkRow4.size()) -// assertEquals(2, backLinkRow5.size()) - -// JSONAssert.assertEquals("[]", getLinksValue(backLinkRow3, 1).toString, JSONCompareMode.LENIENT) -// JSONAssert.assertEquals( -// """[{"id": 1, "value": "table1row1"}]""", -// getLinksValue(backLinkRow4, 1).toString, -// JSONCompareMode.STRICT -// ) -// JSONAssert.assertEquals("[]", getLinksValue(backLinkRow5, 1).toString, JSONCompareMode.LENIENT) -// } -// } -// } - -// @Test -// def changeLink_twoLinksExistingInForeignTable_addAThirdLinkViaFirstTable(implicit c: TestContext): Unit = { -// okTest { -// val putInitialLinksFromTable1 = Json.obj("value" -> Json.obj("values" -> Json.arr(3, 4))) -// val patchThirdLinkFromTable2 = Json.obj("value" -> Json.obj("values" -> Json.arr(1))) - -// for { -// _ <- setupTwoTablesWithEmptyLinks() - -// _ <- sendRequest("PUT", s"/tables/2/columns/3/rows/1", putInitialLinksFromTable1) -// _ <- sendRequest("PATCH", s"/tables/1/columns/3/rows/5", patchThirdLinkFromTable2) - -// linksTable2Rows <- sendRequest("GET", "/tables/2/columns/3/rows/1/history?historyType=cell").map(toRowsArray) - -// linksTable1Rows3 <- sendRequest("GET", "/tables/1/columns/3/rows/3/history?historyType=cell").map(toRowsArray) -// linksTable1Rows4 <- sendRequest("GET", "/tables/1/columns/3/rows/4/history?historyType=cell").map(toRowsArray) -// linksTable1Rows5 <- sendRequest("GET", "/tables/1/columns/3/rows/5/history?historyType=cell").map(toRowsArray) - -// } yield { -// assertEquals(1, linksTable1Rows3.size()) -// assertEquals(1, linksTable1Rows4.size()) -// assertEquals(1, linksTable1Rows5.size()) - -// JSONAssert -// .assertEquals( -// """[{"id": 3}, {"id": 4}]""", -// getLinksValue(linksTable2Rows, 0).toString, -// JSONCompareMode.STRICT_ORDER -// ) -// JSONAssert -// .assertEquals( -// """[{"id": 3}, {"id": 4}, {"id": 5}]""", -// getLinksValue(linksTable2Rows, 1).toString, -// JSONCompareMode.STRICT_ORDER -// ) -// } -// } -// } - -// @Test -// def changeLink_addThreeLinksFromAndToTable(implicit c: TestContext): Unit = { -// okTest { -// val putInitialLinksFromTable1 = Json.obj("value" -> Json.obj("values" -> Json.arr(3, 4, 5))) -// val patchThirdLinkFromTable2 = Json.obj("value" -> Json.obj("values" -> Json.arr(3, 4, 5))) - -// val expectedLinksT1R3 = -// """[ -// | {"id": 4}, -// | {"id": 5}, -// | {"id": 3} -// |]""".stripMargin - -// val expectedLinksT2R3 = -// """[ -// | {"id": 3}, -// | {"id": 4}, -// | {"id": 5} -// |]""".stripMargin - -// val linkToTable2Row3 = """[{"id":3,"value":"table2RowId1"}]""" - -// val linkToTable1Row3 = """[{"id":3,"value":"table1RowId1"}]""" - -// for { -// _ <- setupTwoTablesWithEmptyLinks() - -// _ <- sendRequest("PUT", s"/tables/1/columns/3/rows/3", putInitialLinksFromTable1) -// _ <- sendRequest("PUT", s"/tables/2/columns/3/rows/3", patchThirdLinkFromTable2) - -// linksTable1Rows3 <- sendRequest("GET", "/tables/1/columns/3/rows/3/history?historyType=cell").map(toRowsArray) -// linksTable1Rows4 <- sendRequest("GET", "/tables/1/columns/3/rows/4/history?historyType=cell").map(toRowsArray) -// linksTable1Rows5 <- sendRequest("GET", "/tables/1/columns/3/rows/5/history?historyType=cell").map(toRowsArray) - -// linksTable2Rows3 <- sendRequest("GET", "/tables/2/columns/3/rows/3/history?historyType=cell").map(toRowsArray) -// linksTable2Rows4 <- sendRequest("GET", "/tables/2/columns/3/rows/4/history?historyType=cell").map(toRowsArray) -// linksTable2Rows5 <- sendRequest("GET", "/tables/2/columns/3/rows/5/history?historyType=cell").map(toRowsArray) - -// } yield { -// assertEquals(2, linksTable1Rows3.size()) -// assertEquals(1, linksTable1Rows4.size()) -// assertEquals(1, linksTable1Rows5.size()) - -// assertEquals(2, linksTable2Rows3.size()) -// assertEquals(1, linksTable2Rows4.size()) -// assertEquals(1, linksTable2Rows5.size()) - -// JSONAssert -// .assertEquals(expectedLinksT1R3, getLinksValue(linksTable1Rows3, 1).toString, JSONCompareMode.STRICT_ORDER) -// JSONAssert.assertEquals(linkToTable2Row3, getLinksValue(linksTable1Rows4, 0).toString, JSONCompareMode.LENIENT) -// JSONAssert.assertEquals(linkToTable2Row3, getLinksValue(linksTable1Rows5, 0).toString, JSONCompareMode.LENIENT) - -// JSONAssert -// .assertEquals(expectedLinksT2R3, getLinksValue(linksTable2Rows3, 1).toString, JSONCompareMode.STRICT_ORDER) -// JSONAssert.assertEquals(linkToTable1Row3, getLinksValue(linksTable2Rows4, 0).toString, JSONCompareMode.LENIENT) -// JSONAssert.assertEquals(linkToTable1Row3, getLinksValue(linksTable2Rows5, 0).toString, JSONCompareMode.LENIENT) -// } -// } -// } -// } - -// @RunWith(classOf[VertxUnitRunner]) -// class CreateMultiLanguageLinkHistoryTest extends LinkTestBase with TestHelper { - -// @Test -// def changeLink_MultiIdentifiers_MultiLangAndSingleLangNumeric(implicit c: TestContext): Unit = { -// okTest { - -// val expected = -// """ -// |[ -// | {"id":1,"value":{"de-DE":"Hallo, Table 2 Welt! 3.1415926","en-GB":"Hello, Table 2 World! 3.1415926"}}, -// | {"id":2,"value":{"de-DE":"Hallo, Table 2 Welt2! 2.1415926","en-GB":"Hello, Table 2 World2! 2.1415926"}} -// |] -// |""".stripMargin - -// val postLinkColumn = Json.obj( -// "columns" -> Json.arr( -// Json.obj( -// "name" -> "Test Link 1", -// "kind" -> "link", -// "toTable" -> 2 -// ) -// ) -// ) -// val putLinkValue = Json.obj("value" -> Json.obj("values" -> Json.arr(1, 2))) - -// for { -// _ <- createFullTableWithMultilanguageColumns("Table 1") -// _ <- createFullTableWithMultilanguageColumns("Table 2") - -// // Change target table structure to have a second identifier -// _ <- sendRequest("POST", s"/tables/2/columns/3", Json.obj("identifier" -> true)) - -// // Add link column -// linkColumn <- sendRequest("POST", s"/tables/1/columns", postLinkColumn) -// linkColumnId = linkColumn.getJsonArray("columns").get[JsonObject](0).getNumber("id") - -// _ <- sendRequest("PUT", s"/tables/1/columns/$linkColumnId/rows/1", putLinkValue) -// rows <- sendRequest("GET", "/tables/1/columns/8/rows/1/history?historyType=cell").map(toRowsArray) -// historyAfterCreation = getLinksValue(rows, 0) -// } yield { -// JSONAssert.assertEquals(expected, historyAfterCreation.toString, JSONCompareMode.LENIENT) -// } -// } -// } -// } - -// @RunWith(classOf[VertxUnitRunner]) -// class CreateHistoryCompatibilityTest extends LinkTestBase with TestHelper { -// // For migrated systems it is necessary to also write a history entry for a currently existing cell value - -// @Test -// def changeSimpleValue_firstChangeWithHistoryFeature_shouldCreateInitialHistoryEntry(implicit c: TestContext): Unit = { -// okTest { - -// val sqlConnection = SQLConnection(this.vertxAccess(), databaseConfig) -// val dbConnection = DatabaseConnection(this.vertxAccess(), sqlConnection) - -// val initialValue = """{ "value": "value before history feature" }""" -// val firstChangedValue = """{ "value": "my first change with history feature" }""" - -// for { -// _ <- createEmptyDefaultTable() -// _ <- sendRequest("POST", "/tables/1/rows") - -// // manually insert a value that simulates cell value changes before implementation of the history feature -// _ <- dbConnection.query("""UPDATE -// |user_table_1 -// |SET column_1 = 'value before history feature' -// |WHERE id = 1""".stripMargin) - -// _ <- sendRequest("POST", "/tables/1/columns/1/rows/1", firstChangedValue) -// rows <- sendRequest("GET", "/tables/1/columns/1/rows/1/history?historyType=cell").map(toRowsArray) - -// initialHistoryCreation = rows.get[JsonObject](0) -// firstHistoryCreation = rows.get[JsonObject](1) -// } yield { -// JSONAssert.assertEquals(initialValue, initialHistoryCreation.toString, JSONCompareMode.LENIENT) -// JSONAssert.assertEquals(firstChangedValue, firstHistoryCreation.toString, JSONCompareMode.LENIENT) -// } -// } -// } - -// @Test -// def changeSimpleValue_secondChangeWithHistoryFeature_shouldAgainCreateSingleHistoryEntries( -// implicit c: TestContext -// ): Unit = { -// okTest { -// val sqlConnection = SQLConnection(this.vertxAccess(), databaseConfig) -// val dbConnection = DatabaseConnection(this.vertxAccess(), sqlConnection) - -// val initialValue = """{ "value": "value before history feature" }""" -// val change1 = """{ "value": "first change" }""" -// val change2 = """{ "value": "second change" }""" - -// for { -// _ <- createEmptyDefaultTable() -// _ <- sendRequest("POST", "/tables/1/rows") - -// // manually insert a value that simulates cell value changes before implementation of the history feature -// _ <- dbConnection.query("""UPDATE -// |user_table_1 -// |SET column_1 = 'value before history feature' -// |WHERE id = 1""".stripMargin) - -// _ <- sendRequest("POST", "/tables/1/columns/1/rows/1", change1) -// _ <- sendRequest("POST", "/tables/1/columns/1/rows/1", change2) -// rows <- sendRequest("GET", "/tables/1/columns/1/rows/1/history?historyType=cell").map(toRowsArray) - -// initialHistory = rows.get[JsonObject](0) -// history1 = rows.get[JsonObject](1) -// history2 = rows.get[JsonObject](2) -// } yield { -// JSONAssert.assertEquals(initialValue, initialHistory.toString, JSONCompareMode.LENIENT) -// JSONAssert.assertEquals(change1, history1.toString, JSONCompareMode.LENIENT) -// JSONAssert.assertEquals(change2, history2.toString, JSONCompareMode.LENIENT) -// } -// } -// } - -// @Test -// def changeMultilanguageValue_firstChangeWithHistoryFeature_shouldCreateInitialHistoryEntry( -// implicit c: TestContext -// ): Unit = { -// okTest { -// val sqlConnection = SQLConnection(this.vertxAccess(), databaseConfig) -// val dbConnection = DatabaseConnection(this.vertxAccess(), sqlConnection) - -// val initialValueDE = """{ "value": { "de-DE": "de-DE init" } }""" -// val initialValueEN = """{ "value": { "en-GB": "en-GB init" } }""" -// val change1 = """{ "value": { "de-DE": "de-DE first change" } }""" -// val change2 = """{ "value": { "de-DE": "de-DE second change" } }""" -// val change3 = """{ "value": { "en-GB": "en-GB first change" } }""" - -// for { -// _ <- createTableWithMultilanguageColumns("history test") -// _ <- sendRequest("POST", "/tables/1/rows") - -// // manually insert a value that simulates cell value changes before implementation of the history feature -// _ <- dbConnection.query("""INSERT INTO user_table_lang_1(id, langtag,column_1) -// | VALUES -// |(1, E'de-DE', E'de-DE init'), -// |(1, E'en-GB', E'en-GB init') -// |""".stripMargin) - -// _ <- sendRequest("POST", "/tables/1/columns/1/rows/1", change1) -// _ <- sendRequest("POST", "/tables/1/columns/1/rows/1", change2) -// _ <- sendRequest("POST", "/tables/1/columns/1/rows/1", change3) -// rows <- sendRequest("GET", "/tables/1/columns/1/rows/1/history?historyType=cell").map(toRowsArray) - -// initialHistoryDE = rows.getJsonObject(0) -// history1 = rows.getJsonObject(1) -// history2 = rows.getJsonObject(2) -// initialHistoryEN = rows.getJsonObject(3) -// history3 = rows.getJsonObject(4) -// } yield { -// JSONAssert.assertEquals(initialValueDE, initialHistoryDE.toString, JSONCompareMode.LENIENT) -// JSONAssert.assertEquals(change1, history1.toString, JSONCompareMode.LENIENT) -// JSONAssert.assertEquals(change2, history2.toString, JSONCompareMode.LENIENT) -// JSONAssert.assertEquals(initialValueEN, initialHistoryEN.toString, JSONCompareMode.LENIENT) -// JSONAssert.assertEquals(change3, history3.toString, JSONCompareMode.LENIENT) -// } -// } -// } - -// @Test -// def changeCurrencyPOST_firstChangeWithHistoryFeature_shouldCreateInitialHistoryEntry( -// implicit c: TestContext -// ): Unit = { -// okTest { -// val sqlConnection = SQLConnection(this.vertxAccess(), databaseConfig) -// val dbConnection = DatabaseConnection(this.vertxAccess(), sqlConnection) - -// val expectedValues = -// """[ -// | {"value": {"DE": 11}}, -// | {"value": {"GB": 22}}, -// | {"value": {"DE": 33}}, -// | {"value": {"GB": 44}} -// |]""".stripMargin - -// val multiCountryCurrencyColumn = MultiCountry(CurrencyCol("currency-column"), Seq("DE", "GB")) - -// for { -// _ <- createSimpleTableWithCell("table1", multiCountryCurrencyColumn) - -// // manually insert a value that simulates cell value changes before implementation of the history feature -// _ <- dbConnection.query("""INSERT INTO user_table_lang_1(id, langtag, column_1) -// | VALUES -// |(1, E'DE', 11), -// |(1, E'GB', 22) -// |""".stripMargin) - -// _ <- sendRequest("POST", s"/tables/1/columns/1/rows/1", """{"value": {"DE": 33, "GB": 44}}""") -// rows <- sendRequest("GET", "/tables/1/columns/1/rows/1/history?historyType=cell").map(toRowsArray) -// } yield { -// JSONAssert.assertEquals(expectedValues, rows.toString, JSONCompareMode.LENIENT) -// } -// } -// } - -// @Test -// def changeCurrencyPUT_firstChangeWithHistoryFeature_shouldCreateInitialHistoryEntry(implicit c: TestContext): Unit = { -// okTest { -// val sqlConnection = SQLConnection(this.vertxAccess(), databaseConfig) -// val dbConnection = DatabaseConnection(this.vertxAccess(), sqlConnection) - -// val expectedValues = -// """[ -// | {"value": {"DE": 11}}, -// | {"value": {"GB": 22}}, -// | {"value": {"DE": 33}}, -// | {"value": {"GB": 44}} -// |]""".stripMargin - -// val multiCountryCurrencyColumn = MultiCountry(CurrencyCol("currency-column"), Seq("DE", "GB")) - -// for { -// _ <- createSimpleTableWithCell("table1", multiCountryCurrencyColumn) - -// // manually insert a value that simulates cell value changes before implementation of the history feature -// _ <- dbConnection.query("""INSERT INTO user_table_lang_1(id, langtag, column_1) -// | VALUES -// |(1, E'DE', 11), -// |(1, E'GB', 22) -// |""".stripMargin) - -// _ <- sendRequest("PUT", s"/tables/1/columns/1/rows/1", """{"value": {"DE": 33, "GB": 44}}""") -// rows <- sendRequest("GET", "/tables/1/columns/1/rows/1/history?historyType=cell").map(toRowsArray) -// } yield { -// JSONAssert.assertEquals(expectedValues, rows.toString, JSONCompareMode.LENIENT) -// } -// } -// } - -// @Test -// def changeLinkValue_firstChangeWithHistoryFeature_shouldCreateInitialHistoryEntry(implicit c: TestContext): Unit = { -// okTest { -// val sqlConnection = SQLConnection(this.vertxAccess(), databaseConfig) -// val dbConnection = DatabaseConnection(this.vertxAccess(), sqlConnection) - -// val expectedInitialLinks = """{ "value": [ {"id":3}, {"id": 4} ] }""" -// val expectedAfterPostLinks = """{ "value": [ {"id":3}, {"id": 4}, {"id": 5} ] }""" - -// for { -// linkColumnId <- setupTwoTablesWithEmptyLinks() - -// _ <- dbConnection.query("""INSERT INTO link_table_1 -// | (id_1, id_2) -// |VALUES -// | (1, 3), -// | (1, 4) -// | """.stripMargin) - -// _ <- sendRequest("POST", s"/tables/1/columns/$linkColumnId/rows/1", """{ "value": [ 5 ] }""") -// rows <- sendRequest("GET", s"/tables/1/columns/$linkColumnId/rows/1/history?historyType=cell").map(toRowsArray) -// initialHistory = rows.getJsonObject(0) -// history1 = rows.getJsonObject(1) -// } yield { -// JSONAssert.assertEquals(expectedInitialLinks, initialHistory.toString, JSONCompareMode.LENIENT) -// JSONAssert.assertEquals(expectedAfterPostLinks, history1.toString, JSONCompareMode.LENIENT) -// } -// } -// } - -// @Test -// def changeLinkValue_twoLinkChanges_onlyFirstOneShouldCreateInitialHistoryEntry(implicit c: TestContext): Unit = { -// okTest { -// val sqlConnection = SQLConnection(this.vertxAccess(), databaseConfig) -// val dbConnection = DatabaseConnection(this.vertxAccess(), sqlConnection) - -// val expectedInitialLinks = """{ "value": [ {"id":3} ] }""" -// val expectedAfterPostLinks1 = """{ "value": [ {"id":3}, {"id":4} ] }""" -// val expectedAfterPostLinks2 = """{ "value": [ {"id":3}, {"id":4}, {"id":5} ] }""" - -// for { -// linkColumnId <- setupTwoTablesWithEmptyLinks() - -// _ <- dbConnection.query("""INSERT INTO link_table_1 -// | (id_1, id_2) -// |VALUES -// | (1, 3) -// | """.stripMargin) - -// _ <- sendRequest("POST", s"/tables/1/columns/$linkColumnId/rows/1", """{ "value": [ 4 ] }""") -// _ <- sendRequest("POST", s"/tables/1/columns/$linkColumnId/rows/1", """{ "value": [ 5 ] }""") -// rows <- sendRequest("GET", s"/tables/1/columns/$linkColumnId/rows/1/history?historyType=cell").map(toRowsArray) - -// initialHistory = rows.getJsonObject(0) -// history1 = rows.getJsonObject(1) -// history2 = rows.getJsonObject(2) -// } yield { -// JSONAssert.assertEquals(expectedInitialLinks, initialHistory.toString, JSONCompareMode.LENIENT) -// JSONAssert.assertEquals(expectedAfterPostLinks1, history1.toString, JSONCompareMode.LENIENT) -// JSONAssert.assertEquals(expectedAfterPostLinks2, history2.toString, JSONCompareMode.LENIENT) -// } -// } -// } - -// @Test -// def deleteLinkValue_threeLinks_deleteOneOfThem(implicit c: TestContext): Unit = { -// okTest { -// val sqlConnection = SQLConnection(this.vertxAccess(), databaseConfig) -// val dbConnection = DatabaseConnection(this.vertxAccess(), sqlConnection) - -// val expectedInitialLinks = """{ "value": [ {"id":3}, {"id":4}, {"id":5} ] }""" -// val expectedAfterPostLinks1 = """{ "value": [ {"id":3}, {"id":5} ] }""" - -// for { -// linkColumnId <- setupTwoTablesWithEmptyLinks() - -// _ <- dbConnection.query("""INSERT INTO link_table_1 -// | (id_1, id_2) -// |VALUES -// | (1, 3), -// | (1, 4), -// | (1, 5) -// | """.stripMargin) - -// _ <- sendRequest("DELETE", s"/tables/1/columns/$linkColumnId/rows/1/link/4") -// rows <- sendRequest("GET", s"/tables/1/columns/$linkColumnId/rows/1/history?historyType=cell").map(toRowsArray) - -// initialHistory = rows.getJsonObject(0) -// history1 = rows.getJsonObject(1) -// } yield { -// JSONAssert.assertEquals(expectedInitialLinks, initialHistory.toString, JSONCompareMode.LENIENT) -// JSONAssert.assertEquals(expectedAfterPostLinks1, history1.toString, JSONCompareMode.LENIENT) -// } -// } -// } - -// @Test -// def changeLinkOrder_reverseOrderInTwoSteps_createOnlyOneInitHistory(implicit c: TestContext): Unit = { -// okTest { -// val sqlConnection = SQLConnection(this.vertxAccess(), databaseConfig) -// val dbConnection = DatabaseConnection(this.vertxAccess(), sqlConnection) - -// val expectedInitialLinks = """{ "value": [ {"id":3}, {"id":4}, {"id":5} ] }""" -// val expectedAfterPostLinks1 = """{ "value": [ {"id":4}, {"id":5}, {"id":3} ] }""" -// val expectedAfterPostLinks2 = """{ "value": [ {"id":5}, {"id":4}, {"id":3} ] }""" - -// for { -// linkColumnId <- setupTwoTablesWithEmptyLinks() - -// _ <- dbConnection.query("""INSERT INTO link_table_1 -// | (id_1, id_2) -// |VALUES -// | (1, 3), -// | (1, 4), -// | (1, 5) -// | """.stripMargin) - -// _ <- sendRequest("PUT", s"/tables/1/columns/$linkColumnId/rows/1/link/3/order", s""" {"location": "end"} """) -// _ <- sendRequest("PUT", s"/tables/1/columns/$linkColumnId/rows/1/link/5/order", s""" {"location": "start"} """) - -// rows <- sendRequest("GET", s"/tables/1/columns/$linkColumnId/rows/1/history?historyType=cell").map(toRowsArray) - -// initialHistory = rows.getJsonObject(0) -// history1 = rows.getJsonObject(1) -// history2 = rows.getJsonObject(2) -// } yield { -// JSONAssert.assertEquals(expectedInitialLinks, initialHistory.toString, JSONCompareMode.LENIENT) -// JSONAssert.assertEquals(expectedAfterPostLinks1, history1.toString, JSONCompareMode.LENIENT) -// JSONAssert.assertEquals(expectedAfterPostLinks2, history2.toString, JSONCompareMode.LENIENT) -// } -// } -// } - -// @Test -// def deleteLinkValue_threeLinks_deleteTwoTimesOnlyOneInitHistory(implicit c: TestContext): Unit = { -// okTest { -// val sqlConnection = SQLConnection(this.vertxAccess(), databaseConfig) -// val dbConnection = DatabaseConnection(this.vertxAccess(), sqlConnection) - -// val expectedInitialLinks = """{ "value": [ {"id":3}, {"id":4}, {"id":5} ] }""" -// val expectedAfterPostLinks1 = """{ "value": [ {"id":3}, {"id":5} ] }""" -// val expectedAfterPostLinks2 = """{ "value": [ {"id":5} ] }""" - -// for { -// linkColumnId <- setupTwoTablesWithEmptyLinks() - -// _ <- dbConnection.query("""INSERT INTO link_table_1 -// | (id_1, id_2) -// |VALUES -// | (1, 3), -// | (1, 4), -// | (1, 5) -// | """.stripMargin) - -// _ <- sendRequest("DELETE", s"/tables/1/columns/$linkColumnId/rows/1/link/4") -// _ <- sendRequest("DELETE", s"/tables/1/columns/$linkColumnId/rows/1/link/3") -// rows <- sendRequest("GET", s"/tables/1/columns/$linkColumnId/rows/1/history?historyType=cell").map(toRowsArray) - -// initialHistory = rows.getJsonObject(0) -// history1 = rows.getJsonObject(1) -// history2 = rows.getJsonObject(2) -// } yield { -// JSONAssert.assertEquals(expectedInitialLinks, initialHistory.toString, JSONCompareMode.LENIENT) -// JSONAssert.assertEquals(expectedAfterPostLinks1, history1.toString, JSONCompareMode.LENIENT) -// JSONAssert.assertEquals(expectedAfterPostLinks2, history2.toString, JSONCompareMode.LENIENT) -// } -// } -// } - -// @Test -// def changeSimpleValue_booleanInitHistoryWithValueFalse(implicit c: TestContext): Unit = { -// okTest { -// // Booleans always gets a initial history entry on first change -// val expectedInitialLinks = """{ "value": false} """ -// val expectedAfterPost1 = """{ "value": true }""" - -// val booleanColumn = -// s"""{"columns": [{"kind": "boolean", "name": "Boolean Column", "languageType": "neutral"} ] }""" - -// for { -// _ <- createEmptyDefaultTable("history test") - -// // create simple boolean column -// _ <- sendRequest("POST", "/tables/1/columns", booleanColumn) - -// _ <- sendRequest("POST", "/tables/1/rows") -// _ <- sendRequest("POST", "/tables/1/columns/3/rows/1", expectedAfterPost1) -// rows <- sendRequest("GET", "/tables/1/columns/3/rows/1/history?historyType=cell").map(toRowsArray) - -// initialHistory = rows.getJsonObject(0) -// history1 = rows.getJsonObject(1) -// } yield { -// JSONAssert.assertEquals(expectedInitialLinks, initialHistory.toString, JSONCompareMode.LENIENT) -// JSONAssert.assertEquals(expectedAfterPost1, history1.toString, JSONCompareMode.LENIENT) -// } -// } -// } - -// @Test -// def changeSimpleValue_booleanInitHistoryWithSameValue(implicit c: TestContext): Unit = { -// okTest { -// val sqlConnection = SQLConnection(this.vertxAccess(), databaseConfig) -// val dbConnection = DatabaseConnection(this.vertxAccess(), sqlConnection) - -// // Booleans always gets a initial history entry on first change -// val expectedInitialLinks = """{ "value": true} """ -// val expectedAfterPost1 = """{ "value": true }""" - -// val booleanColumn = -// s"""{"columns": [{"kind": "boolean", "name": "Boolean Column", "languageType": "neutral"} ] }""" - -// for { -// _ <- createEmptyDefaultTable("history test") - -// // create simple boolean column -// _ <- sendRequest("POST", "/tables/1/columns", booleanColumn) - -// _ <- sendRequest("POST", "/tables/1/rows") - -// // manually update value that simulates cell value changes before implementation of the history feature -// _ <- dbConnection.query("""UPDATE user_table_1 SET column_3 = TRUE WHERE id = 1""".stripMargin) - -// _ <- sendRequest("POST", "/tables/1/columns/3/rows/1", expectedAfterPost1) -// rows <- sendRequest("GET", "/tables/1/columns/3/rows/1/history?historyType=cell").map(toRowsArray) - -// initialHistory = rows.getJsonObject(0) -// history1 = rows.getJsonObject(1) -// } yield { -// JSONAssert.assertEquals(expectedInitialLinks, initialHistory.toString, JSONCompareMode.LENIENT) -// JSONAssert.assertEquals(expectedAfterPost1, history1.toString, JSONCompareMode.LENIENT) -// } -// } -// } - -// @Test -// def changeSimpleValue_boolean(implicit c: TestContext): Unit = { -// okTest { -// // Booleans always gets a initial history entry on first change -// val expectedInitialLinks = """{ "value": false} """ -// val expectedAfterPost1 = """{ "value": true }""" -// val expectedAfterPost2 = """{ "value": false }""" - -// val booleanColumn = -// s"""{"columns": [{"kind": "boolean", "name": "Boolean Column", "languageType": "neutral"} ] }""" - -// for { -// _ <- createEmptyDefaultTable("history test") - -// // create simple boolean column -// _ <- sendRequest("POST", "/tables/1/columns", booleanColumn) - -// _ <- sendRequest("POST", "/tables/1/rows") -// _ <- sendRequest("POST", "/tables/1/columns/3/rows/1", expectedAfterPost1) -// _ <- sendRequest("POST", "/tables/1/columns/3/rows/1", expectedAfterPost2) -// rows <- sendRequest("GET", "/tables/1/columns/3/rows/1/history?historyType=cell").map(toRowsArray) - -// initialHistory = rows.getJsonObject(0) -// history1 = rows.getJsonObject(1) -// history2 = rows.getJsonObject(2) -// } yield { -// JSONAssert.assertEquals(expectedInitialLinks, initialHistory.toString, JSONCompareMode.LENIENT) -// JSONAssert.assertEquals(expectedAfterPost1, history1.toString, JSONCompareMode.LENIENT) -// JSONAssert.assertEquals(expectedAfterPost2, history2.toString, JSONCompareMode.LENIENT) -// } -// } -// } - -// @Test -// def changeMultilanguageValue_boolean(implicit c: TestContext): Unit = { -// okTest { -// // Booleans always gets a initial history entry on first change -// val expectedInitialLinks = """{ "value": {"de-DE": false} }""" -// val expectedAfterPost1 = """{ "value": {"de-DE": true} }""" -// val expectedAfterPost2 = """{ "value": {"de-DE": false} }""" - -// for { -// _ <- createTableWithMultilanguageColumns("history test") -// _ <- sendRequest("POST", "/tables/1/rows") -// _ <- sendRequest("POST", "/tables/1/columns/2/rows/1", expectedAfterPost1) -// _ <- sendRequest("POST", "/tables/1/columns/2/rows/1", expectedAfterPost2) -// rows <- sendRequest("GET", "/tables/1/columns/2/rows/1/history?historyType=cell").map(toRowsArray) - -// initialHistory = rows.getJsonObject(0) -// history1 = rows.getJsonObject(1) -// history2 = rows.getJsonObject(2) -// } yield { -// JSONAssert.assertEquals(expectedInitialLinks, initialHistory.toString, JSONCompareMode.LENIENT) -// JSONAssert.assertEquals(expectedAfterPost1, history1.toString, JSONCompareMode.LENIENT) -// JSONAssert.assertEquals(expectedAfterPost2, history2.toString, JSONCompareMode.LENIENT) -// } -// } -// } - -// @Test -// def deleteSimpleCell_firstChangeWithHistoryFeature_shouldCreateInitialHistoryEntry(implicit c: TestContext): Unit = { -// okTest { - -// val sqlConnection = SQLConnection(this.vertxAccess(), databaseConfig) -// val dbConnection = DatabaseConnection(this.vertxAccess(), sqlConnection) - -// val initialValue = """{ "value": "value before history feature" }""" - -// for { -// _ <- createEmptyDefaultTable() -// _ <- sendRequest("POST", "/tables/1/rows") - -// // manually insert a value that simulates cell value changes before implementation of the history feature -// _ <- dbConnection.query("""UPDATE -// |user_table_1 -// |SET column_1 = 'value before history feature' -// |WHERE id = 1""".stripMargin) - -// _ <- sendRequest("DELETE", "/tables/1/columns/1/rows/1") -// rows <- sendRequest("GET", "/tables/1/columns/1/rows/1/history?historyType=cell").map(toRowsArray) - -// initialHistoryCreation = rows.get[JsonObject](0) -// firstHistoryCreation = rows.get[JsonObject](1) -// } yield { -// JSONAssert.assertEquals(initialValue, initialHistoryCreation.toString, JSONCompareMode.LENIENT) -// JSONAssert.assertEquals("""{ "value": null }""", firstHistoryCreation.toString, JSONCompareMode.LENIENT) -// } -// } -// } - -// @Test -// def deleteMultilanguageCell_firstChangeWithHistoryFeature_shouldCreateInitialHistoryEntry( -// implicit c: TestContext -// ): Unit = { -// okTest { -// val sqlConnection = SQLConnection(this.vertxAccess(), databaseConfig) -// val dbConnection = DatabaseConnection(this.vertxAccess(), sqlConnection) - -// val initialValueDE = """{ "value": { "de-DE": "de-DE init" } }""" - -// for { -// _ <- createTableWithMultilanguageColumns("history test") -// _ <- sendRequest("POST", "/tables/1/rows") - -// // manually insert a value that simulates cell value changes before implementation of the history feature -// _ <- dbConnection.query("""INSERT INTO user_table_lang_1(id, langtag, column_1) -// | VALUES -// |(1, E'de-DE', E'de-DE init'), -// |(1, E'en-GB', E'en-GB init') -// |""".stripMargin) - -// _ <- sendRequest("DELETE", "/tables/1/columns/1/rows/1") -// rows <- sendRequest("GET", "/tables/1/columns/1/rows/1/history/de-DE?historyType=cell").map(toRowsArray) - -// initialHistoryDE = rows.getJsonObject(0) -// history = rows.getJsonObject(1) -// } yield { -// JSONAssert.assertEquals(initialValueDE, initialHistoryDE.toString, JSONCompareMode.LENIENT) -// JSONAssert.assertEquals("""{ "value": { "de-DE": null } }""", history.toString, JSONCompareMode.LENIENT) -// } -// } -// } - -// @Test -// def deleteLinkCell_threeLinks_deleteTwoTimesOnlyOneInitHistory(implicit c: TestContext): Unit = { -// okTest { -// val sqlConnection = SQLConnection(this.vertxAccess(), databaseConfig) -// val dbConnection = DatabaseConnection(this.vertxAccess(), sqlConnection) - -// val expectedInitialLinks = """[ {"id":3}, {"id":4}, {"id":5} ]""" - -// for { -// linkColumnId <- setupTwoTablesWithEmptyLinks() - -// // manually insert a value that simulates cell value changes before implementation of the history feature -// _ <- dbConnection.query("""INSERT INTO link_table_1 -// | (id_1, id_2) -// |VALUES -// | (1, 3), -// | (1, 4), -// | (1, 5) -// | """.stripMargin) - -// _ <- sendRequest("DELETE", s"/tables/1/columns/$linkColumnId/rows/1") -// rows <- sendRequest("GET", "/tables/1/columns/3/rows/1/history?historyType=cell").map(toRowsArray) -// initialHistory = getLinksValue(rows, 0) -// history = getLinksValue(rows, 1) -// } yield { -// JSONAssert.assertEquals(expectedInitialLinks, initialHistory.toString, JSONCompareMode.LENIENT) -// JSONAssert.assertEquals("[]", history.toString, JSONCompareMode.LENIENT) -// } -// } -// } - -// @Test -// def addAttachment_firstChangeWithHistoryFeature_shouldCreateInitialHistoryEntry(implicit c: TestContext): Unit = { -// okTest { -// val sqlConnection = SQLConnection(this.vertxAccess(), databaseConfig) -// val dbConnection = DatabaseConnection(this.vertxAccess(), sqlConnection) -// val attachmentColumn = """{"columns": [{"kind": "attachment", "name": "Downloads"}] }""" - -// for { -// _ <- createDefaultTable() -// _ <- sendRequest("POST", s"/tables/1/columns", attachmentColumn) - -// fileUuid1 <- createTestAttachment("Test 1") -// fileUuid2 <- createTestAttachment("Test 2") - -// // manually insert a value that simulates cell value changes before implementation of the history feature -// _ <- dbConnection.query(s"""INSERT INTO system_attachment -// | (table_id, column_id, row_id, attachment_uuid, ordering) -// |VALUES -// | (1, 3, 1, '$fileUuid1', 1) -// | """.stripMargin) - -// _ <- sendRequest("POST", s"/tables/1/columns/3/rows/1", Json.obj("value" -> Json.obj("uuid" -> fileUuid2))) -// rows <- sendRequest("GET", "/tables/1/columns/3/rows/1/history?historyType=cell").map(toRowsArray) -// initialHistory = rows.get[JsonObject](0).getJsonArray("value").getJsonObject(0) -// history = rows.get[JsonObject](1).getJsonArray("value").getJsonObject(0) -// } yield { -// assertJSONEquals(Json.obj("uuid" -> fileUuid1), initialHistory, JSONCompareMode.LENIENT) -// assertJSONEquals(Json.obj("uuid" -> fileUuid1), history, JSONCompareMode.LENIENT) -// } -// } -// } - -// @Test -// def addAttachment_twoTimes_shouldOnlyCreateOneInitialHistoryEntry(implicit c: TestContext): Unit = { -// okTest { -// val sqlConnection = SQLConnection(this.vertxAccess(), databaseConfig) -// val dbConnection = DatabaseConnection(this.vertxAccess(), sqlConnection) -// val attachmentColumn = """{"columns": [{"kind": "attachment", "name": "Downloads"}] }""" - -// for { -// _ <- createDefaultTable() -// _ <- sendRequest("POST", s"/tables/1/columns", attachmentColumn) - -// fileUuid1 <- createTestAttachment("Test 1") -// fileUuid2 <- createTestAttachment("Test 2") -// fileUuid3 <- createTestAttachment("Test 3") - -// // manually insert a value that simulates cell value changes before implementation of the history feature -// _ <- dbConnection.query(s"""INSERT INTO system_attachment -// | (table_id, column_id, row_id, attachment_uuid, ordering) -// |VALUES -// | (1, 3, 1, '$fileUuid1', 1) -// | """.stripMargin) - -// _ <- sendRequest("POST", s"/tables/1/columns/3/rows/1", Json.obj("value" -> Json.obj("uuid" -> fileUuid2))) -// _ <- sendRequest("POST", s"/tables/1/columns/3/rows/1", Json.obj("value" -> Json.obj("uuid" -> fileUuid3))) -// rows <- sendRequest("GET", "/tables/1/columns/3/rows/1/history?historyType=cell").map(toRowsArray) -// } yield { -// assertEquals(3, rows.size()) -// } -// } -// } - -// @Test -// def deleteAttachmentCell_shouldCreateInitialHistoryEntry(implicit c: TestContext): Unit = { -// okTest { -// val sqlConnection = SQLConnection(this.vertxAccess(), databaseConfig) -// val dbConnection = DatabaseConnection(this.vertxAccess(), sqlConnection) -// val attachmentColumn = """{"columns": [{"kind": "attachment", "name": "Downloads"}] }""" - -// for { -// _ <- createDefaultTable() -// _ <- sendRequest("POST", s"/tables/1/columns", attachmentColumn) - -// fileUuid1 <- createTestAttachment("Test 1") -// fileUuid2 <- createTestAttachment("Test 2") - -// // manually insert a value that simulates cell value changes before implementation of the history feature -// _ <- dbConnection.query(s"""INSERT INTO system_attachment -// | (table_id, column_id, row_id, attachment_uuid, ordering) -// |VALUES -// | (1, 3, 1, '$fileUuid1', 1), -// | (1, 3, 1, '$fileUuid2', 2) -// | """.stripMargin) - -// _ <- sendRequest("DELETE", "/tables/1/columns/3/rows/1") - -// rows <- sendRequest("GET", "/tables/1/columns/3/rows/1/history?historyType=cell").map(toRowsArray) -// initialCountBeforeDeletion = rows.get[JsonObject](0).getJsonArray("value") -// attachmentCountAfterDeletion = rows.get[JsonObject](1).getJsonArray("value") -// } yield { -// assertEquals(2, initialCountBeforeDeletion.size()) -// assertEquals(0, attachmentCountAfterDeletion.size()) -// } -// } -// } - -// } - -// @RunWith(classOf[VertxUnitRunner]) -// class CreateAttachmentHistoryTest extends MediaTestBase with TestHelper { - -// @Test -// def addAttachment_toEmptyCell(implicit c: TestContext): Unit = { -// okTest { -// val attachmentColumn = """{"columns": [{"kind": "attachment", "name": "Downloads"}] }""" - -// for { -// _ <- createDefaultTable() -// _ <- sendRequest("POST", s"/tables/1/columns", attachmentColumn) - -// fileUuid <- createTestAttachment("Test 1") - -// _ <- sendRequest("POST", s"/tables/1/columns/3/rows/1", Json.obj("value" -> Json.obj("uuid" -> fileUuid))) -// rows <- sendRequest("GET", "/tables/1/columns/3/rows/1/history?historyType=cell").map(toRowsArray) -// currentUuid = rows.get[JsonObject](0).getJsonArray("value").getJsonObject(0).getString("uuid") -// } yield { -// assertEquals(fileUuid, currentUuid) -// } -// } -// } - -// @Test -// def addAttachment_toCellContainingOneAttachment(implicit c: TestContext): Unit = { -// okTest { -// val attachmentColumn = """{"columns": [{"kind": "attachment", "name": "Downloads"}] }""" - -// for { -// _ <- createDefaultTable() -// _ <- sendRequest("POST", s"/tables/1/columns", attachmentColumn) - -// fileUuid1 <- createTestAttachment("Test 1") -// fileUuid2 <- createTestAttachment("Test 2") - -// _ <- sendRequest("POST", s"/tables/1/columns/3/rows/1", Json.obj("value" -> Json.obj("uuid" -> fileUuid1))) -// _ <- sendRequest("POST", s"/tables/1/columns/3/rows/1", Json.obj("value" -> Json.obj("uuid" -> fileUuid2))) -// rows <- sendRequest("GET", "/tables/1/columns/3/rows/1/history?historyType=cell").map(toRowsArray) -// firstHistory = rows.get[JsonObject](0).getJsonArray("value").getJsonObject(0) -// secondHistory1 = rows.get[JsonObject](1).getJsonArray("value").getJsonObject(0) -// secondHistory2 = rows.get[JsonObject](1).getJsonArray("value").getJsonObject(1) -// } yield { -// assertJSONEquals(Json.obj("uuid" -> fileUuid1), firstHistory, JSONCompareMode.LENIENT) -// assertJSONEquals(Json.obj("uuid" -> fileUuid1), secondHistory1, JSONCompareMode.LENIENT) -// assertJSONEquals(Json.obj("uuid" -> fileUuid2), secondHistory2, JSONCompareMode.LENIENT) -// } -// } -// } - -// @Test -// def addAttachments_addThreeAttachmentsAtOnce(implicit c: TestContext): Unit = { -// okTest { -// val attachmentColumn = """{"columns": [{"kind": "attachment", "name": "Downloads"}] }""" - -// for { -// _ <- createDefaultTable() -// _ <- sendRequest("POST", s"/tables/1/columns", attachmentColumn) - -// fileUuid1 <- createTestAttachment("Test 1") -// fileUuid2 <- createTestAttachment("Test 2") - -// _ <- sendRequest("POST", s"/tables/1/columns/3/rows/1", Json.obj("value" -> Json.arr(fileUuid1, fileUuid2))) - -// rows <- sendRequest("GET", "/tables/1/columns/3/rows/1/history?historyType=cell").map(toRowsArray) -// history1 = rows.get[JsonObject](0).getJsonArray("value").getJsonObject(0) -// history2 = rows.get[JsonObject](0).getJsonArray("value").getJsonObject(1) -// } yield { -// assertJSONEquals(Json.obj("uuid" -> fileUuid1), history1, JSONCompareMode.LENIENT) -// assertJSONEquals(Json.obj("uuid" -> fileUuid2), history2, JSONCompareMode.LENIENT) -// } -// } -// } - -// @Test -// def deleteAttachment_fromCellContainingTwoAttachments(implicit c: TestContext): Unit = { -// okTest { -// val attachmentColumn = """{"columns": [{"kind": "attachment", "name": "Downloads"}] }""" - -// for { -// _ <- createDefaultTable() -// _ <- sendRequest("POST", s"/tables/1/columns", attachmentColumn) - -// fileUuid1 <- createTestAttachment("Test 1") -// fileUuid2 <- createTestAttachment("Test 2") - -// _ <- sendRequest("POST", s"/tables/1/columns/3/rows/1", Json.obj("value" -> Json.arr(fileUuid1, fileUuid2))) -// _ <- sendRequest("DELETE", s"/tables/1/columns/3/rows/1/attachment/$fileUuid1") - -// rows <- sendRequest("GET", "/tables/1/columns/3/rows/1/history?historyType=cell").map(toRowsArray) - -// afterDeletionHistory = rows.get[JsonObject](1).getJsonArray("value").getJsonObject(0) -// } yield { - -// assertJSONEquals(Json.obj("uuid" -> fileUuid2), afterDeletionHistory, JSONCompareMode.LENIENT) -// } -// } -// } - -// @Test -// def deleteAttachment_fromCellContainingThreeAttachments(implicit c: TestContext): Unit = { -// okTest { -// val attachmentColumn = """{"columns": [{"kind": "attachment", "name": "Downloads"}] }""" - -// for { -// _ <- createDefaultTable() -// _ <- sendRequest("POST", s"/tables/1/columns", attachmentColumn) - -// fileUuid1 <- createTestAttachment("Test 1") -// fileUuid2 <- createTestAttachment("Test 2") -// fileUuid3 <- createTestAttachment("Test 3") - -// _ <- sendRequest( -// "POST", -// s"/tables/1/columns/3/rows/1", -// Json.obj("value" -> Json.arr(fileUuid1, fileUuid2, fileUuid3)) -// ) -// _ <- sendRequest("DELETE", s"/tables/1/columns/3/rows/1/attachment/$fileUuid2") - -// rows <- sendRequest("GET", "/tables/1/columns/3/rows/1/history?historyType=cell").map(toRowsArray) - -// afterDeletionHistory1 = rows.get[JsonObject](1).getJsonArray("value").getJsonObject(0) -// afterDeletionHistory2 = rows.get[JsonObject](1).getJsonArray("value").getJsonObject(1) -// } yield { -// assertJSONEquals(Json.obj("uuid" -> fileUuid1), afterDeletionHistory1, JSONCompareMode.LENIENT) -// assertJSONEquals(Json.obj("uuid" -> fileUuid3), afterDeletionHistory2, JSONCompareMode.LENIENT) -// } -// } -// } - -// @Test -// def deleteCell_attachment(implicit c: TestContext): Unit = { -// okTest { -// val attachmentColumn = """{"columns": [{"kind": "attachment", "name": "Downloads"}] }""" - -// for { -// _ <- createDefaultTable() -// _ <- sendRequest("POST", s"/tables/1/columns", attachmentColumn) - -// fileUuid1 <- createTestAttachment("Test 1") -// fileUuid2 <- createTestAttachment("Test 2") -// _ <- sendRequest("POST", s"/tables/1/columns/3/rows/1", Json.obj("value" -> Json.arr(fileUuid1, fileUuid2))) - -// _ <- sendRequest("DELETE", "/tables/1/columns/3/rows/1") - -// rows <- sendRequest("GET", "/tables/1/columns/3/rows/1/history?historyType=cell").map(toRowsArray) -// attachmentCountBeforeDeletion = rows.get[JsonObject](0).getJsonArray("value") -// attachmentCountAfterDeletion = rows.get[JsonObject](1).getJsonArray("value") -// } yield { -// assertEquals(2, attachmentCountBeforeDeletion.size()) -// assertEquals(0, attachmentCountAfterDeletion.size()) -// } -// } -// } -// } - -// @RunWith(classOf[VertxUnitRunner]) -// class CreateBidirectionalCompatibilityLinkHistoryTest extends LinkTestBase with TestHelper { - -// @Test -// def changeLink_twoLinksExisting_postOtherLink(implicit c: TestContext): Unit = { -// okTest { -// val sqlConnection = SQLConnection(this.vertxAccess(), databaseConfig) -// val dbConnection = DatabaseConnection(this.vertxAccess(), sqlConnection) - -// val expectedBacklink = """[{"id": 1, "value": "table1row1"}]""" - -// for { -// linkColumnId <- setupTwoTablesWithEmptyLinks() - -// _ <- dbConnection.query("""INSERT INTO link_table_1 -// | (id_1, id_2) -// |VALUES -// | (1, 3), -// | (1, 4) -// | """.stripMargin) - -// _ <- sendRequest("POST", s"/tables/1/columns/$linkColumnId/rows/1", """{ "value": [ 5 ] }""") -// backLinkRow3 <- sendRequest("GET", "/tables/2/columns/3/rows/3/history?historyType=cell").map(toRowsArray) -// backLinkRow4 <- sendRequest("GET", "/tables/2/columns/3/rows/4/history?historyType=cell").map(toRowsArray) -// backLinkRow5 <- sendRequest("GET", "/tables/2/columns/3/rows/5/history?historyType=cell").map(toRowsArray) - -// } yield { -// assertEquals(1, backLinkRow3.size()) -// assertEquals(1, backLinkRow4.size()) -// assertEquals(1, backLinkRow5.size()) - -// JSONAssert.assertEquals(expectedBacklink, getLinksValue(backLinkRow3, 0).toString, JSONCompareMode.LENIENT) -// JSONAssert.assertEquals(expectedBacklink, getLinksValue(backLinkRow4, 0).toString, JSONCompareMode.LENIENT) -// JSONAssert.assertEquals(expectedBacklink, getLinksValue(backLinkRow5, 0).toString, JSONCompareMode.LENIENT) -// } -// } -// } - -// @Test -// def changeLink_twoLinksExisting_putOneOfThem(implicit c: TestContext): Unit = { -// okTest { -// val sqlConnection = SQLConnection(this.vertxAccess(), databaseConfig) -// val dbConnection = DatabaseConnection(this.vertxAccess(), sqlConnection) - -// val expectedBacklink = """[{"id": 1, "value": "table1row1"}]""" - -// for { -// linkColumnId <- setupTwoTablesWithEmptyLinks() - -// _ <- dbConnection.query("""INSERT INTO link_table_1 -// | (id_1, id_2) -// |VALUES -// | (1, 3), -// | (1, 4) -// | """.stripMargin) - -// _ <- sendRequest("PUT", s"/tables/1/columns/$linkColumnId/rows/1", """{ "value": [ 4 ] }""") -// backLinkRow3 <- sendRequest("GET", "/tables/2/columns/3/rows/3/history?historyType=cell").map(toRowsArray) -// backLinkRow4 <- sendRequest("GET", "/tables/2/columns/3/rows/4/history?historyType=cell").map(toRowsArray) -// } yield { -// assertEquals(2, backLinkRow3.size()) -// assertEquals(2, backLinkRow4.size()) - -// JSONAssert.assertEquals(expectedBacklink, getLinksValue(backLinkRow3, 0).toString, JSONCompareMode.LENIENT) -// JSONAssert.assertEquals("[]", getLinksValue(backLinkRow3, 1).toString, JSONCompareMode.LENIENT) - -// JSONAssert.assertEquals(expectedBacklink, getLinksValue(backLinkRow4, 0).toString, JSONCompareMode.LENIENT) -// JSONAssert.assertEquals(expectedBacklink, getLinksValue(backLinkRow4, 1).toString, JSONCompareMode.LENIENT) -// } -// } -// } - -// @Test -// def changeLink_twoLinksExisting_putEmptyLinkArray(implicit c: TestContext): Unit = { -// okTest { -// val sqlConnection = SQLConnection(this.vertxAccess(), databaseConfig) -// val dbConnection = DatabaseConnection(this.vertxAccess(), sqlConnection) - -// val expectedInitialBacklink = """[{"id": 1, "value": "table1row1"}]""" - -// for { -// _ <- setupTwoTablesWithEmptyLinks() - -// _ <- dbConnection.query("""INSERT INTO link_table_1 -// | (id_1, id_2) -// |VALUES -// | (1, 3), -// | (1, 4) -// | """.stripMargin) - -// _ <- sendRequest("PUT", s"/tables/1/columns/3/rows/1", """{ "value": [] }""") -// backLinkRow3 <- sendRequest("GET", "/tables/2/columns/3/rows/3/history?historyType=cell").map(toRowsArray) -// backLinkRow4 <- sendRequest("GET", "/tables/2/columns/3/rows/4/history?historyType=cell").map(toRowsArray) -// } yield { -// assertEquals(2, backLinkRow3.size()) -// assertEquals(2, backLinkRow4.size()) - -// JSONAssert -// .assertEquals(expectedInitialBacklink, getLinksValue(backLinkRow3, 0).toString, JSONCompareMode.LENIENT) -// JSONAssert.assertEquals("[]", getLinksValue(backLinkRow3, 1).toString, JSONCompareMode.LENIENT) - -// JSONAssert -// .assertEquals(expectedInitialBacklink, getLinksValue(backLinkRow4, 0).toString, JSONCompareMode.LENIENT) -// JSONAssert.assertEquals("[]", getLinksValue(backLinkRow4, 1).toString, JSONCompareMode.LENIENT) -// } -// } -// } - -// @Test -// def changeLinkOrder_reverseOrderInTwoSteps_shouldNotCreateBacklinkHistory(implicit c: TestContext): Unit = { -// okTest { -// val sqlConnection = SQLConnection(this.vertxAccess(), databaseConfig) -// val dbConnection = DatabaseConnection(this.vertxAccess(), sqlConnection) - -// for { -// _ <- setupTwoTablesWithEmptyLinks() - -// _ <- dbConnection.query("""INSERT INTO link_table_1 -// | (id_1, id_2) -// |VALUES -// | (1, 3), -// | (1, 4), -// | (1, 5) -// | """.stripMargin) - -// _ <- sendRequest("PUT", s"/tables/1/columns/3/rows/1/link/3/order", s""" {"location": "end"} """) -// _ <- sendRequest("PUT", s"/tables/1/columns/3/rows/1/link/5/order", s""" {"location": "start"} """) + @Test + def changeLinkValueTwice_singleLanguageMultiIdentifiers(implicit c: TestContext): Unit = okTest { + val twoLinks = Json.obj("value" -> Json.obj("values" -> Json.arr(1, 2))) -// backLinkRow3 <- sendRequest("GET", "/tables/2/columns/3/rows/3/history?historyType=cell").map(toRowsArray) -// backLinkRow4 <- sendRequest("GET", "/tables/2/columns/3/rows/4/history?historyType=cell").map(toRowsArray) -// } yield { -// assertEquals(1, backLinkRow3.size()) -// assertEquals(1, backLinkRow4.size()) -// } -// } -// } + for { + linkColumnId <- setupTwoTablesWithEmptyLinks() -// @Test -// def patchLink_oneLinksExisting_deleteCell(implicit c: TestContext): Unit = { -// okTest { -// val sqlConnection = SQLConnection(this.vertxAccess(), databaseConfig) -// val dbConnection = DatabaseConnection(this.vertxAccess(), sqlConnection) + // Change target table structure to have a second identifier + _ <- sendRequest("POST", s"/tables/2/columns/2", Json.obj("identifier" -> true)) -// for { -// _ <- setupTwoTablesWithEmptyLinks() + _ <- sendRequest("PUT", s"/tables/1/columns/$linkColumnId/rows/1", twoLinks) + _ <- sendRequest("PUT", s"/tables/1/columns/$linkColumnId/rows/1", twoLinks) + rows <- sendRequest("GET", s"/tables/1/columns/$linkColumnId/rows/1/history?historyType=cell").map(toRowsArray) + } yield { + assertEquals(1, rows.size()) + } + } -// _ <- dbConnection.query("""INSERT INTO link_table_1 -// | (id_1, id_2) -// |VALUES -// | (3, 3), (3, 4), (4, 3) -// | """.stripMargin) + @Test + def changeLinkValueTwice_addOne(implicit c: TestContext): Unit = okTest { + val putLink = Json.obj("value" -> Json.obj("values" -> Json.arr(5))) -// _ <- sendRequest("PATCH", s"/tables/1/columns/3/rows/3", """{ "value": [ 5 ] }""") + for { + _ <- setupTwoTablesWithEmptyLinks() -// t1r3 <- sendRequest("GET", "/tables/1/columns/3/rows/3/history?historyType=cell").map(toRowsArray) + _ <- sendRequest("PUT", s"/tables/1/columns/3/rows/1", putLink) + _ <- sendRequest("PUT", s"/tables/1/columns/3/rows/1", putLink) -// t2r3 <- sendRequest("GET", "/tables/2/columns/3/rows/3/history?historyType=cell").map(toRowsArray) -// t2r4 <- sendRequest("GET", "/tables/2/columns/3/rows/4/history?historyType=cell").map(toRowsArray) -// t2r5 <- sendRequest("GET", "/tables/2/columns/3/rows/5/history?historyType=cell").map(toRowsArray) -// } yield { -// assertEquals(1, t2r3.size()) -// assertEquals(1, t2r4.size()) -// assertEquals(1, t2r5.size()) + history <- sendRequest("GET", "/tables/1/columns/3/rows/1/history?historyType=cell").map(toRowsArray) + targetHistory <- sendRequest("GET", "/tables/2/columns/3/rows/5/history?historyType=cell").map(toRowsArray) + } yield { + assertEquals(1, history.size()) + assertEquals(1, targetHistory.size()) + } + } -// assertEquals(2, t1r3.size()) + @Test + def changeLink_addTwoLinksAtOnce(implicit c: TestContext): Unit = { + okTest { + val putLink = Json.obj("value" -> Json.obj("values" -> Json.arr(4, 5))) -// assertJSONEquals("""[{"id": 3}, {"id": 4}]""", getLinksValue(t2r3, 0).toString, JSONCompareMode.STRICT_ORDER) -// assertJSONEquals("""[{"id": 3}]""", getLinksValue(t2r4, 0).toString, JSONCompareMode.STRICT_ORDER) -// assertJSONEquals("""[{"id": 3}]""", getLinksValue(t2r5, 0).toString, JSONCompareMode.STRICT_ORDER) + for { + _ <- setupTwoTablesWithEmptyLinks() -// assertJSONEquals("""[{"id": 3}, {"id": 4}]""", getLinksValue(t1r3, 0).toString, JSONCompareMode.STRICT_ORDER) -// assertJSONEquals( -// """[{"id": 3}, {"id": 4}, {"id": 5}]""", -// getLinksValue(t1r3, 1).toString, -// JSONCompareMode.STRICT_ORDER -// ) -// } -// } -// } + _ <- sendRequest("PUT", s"/tables/1/columns/3/rows/1", putLink) + _ <- sendRequest("PUT", s"/tables/1/columns/3/rows/1", putLink) + history <- sendRequest("GET", "/tables/1/columns/3/rows/1/history?historyType=cell").map(toRowsArray) + targetHistory1 <- sendRequest("GET", "/tables/2/columns/3/rows/4/history?historyType=cell").map(toRowsArray) + targetHistory2 <- sendRequest("GET", "/tables/2/columns/3/rows/5/history?historyType=cell").map(toRowsArray) + } yield { + assertEquals(1, history.size()) + assertEquals(1, targetHistory1.size()) + assertEquals(1, targetHistory2.size()) + } + } + } } From 70e06574e3f9ece821c88f9a73f3c8900dcf0209 Mon Sep 17 00:00:00 2001 From: Manfred Zingl Date: Tue, 24 Oct 2023 16:15:18 +0200 Subject: [PATCH 7/9] add optional query param to force history --- src/main/resources/swagger.json | 17 ++ .../controller/TableauxController.scala | 8 +- .../database/model/TableauxModel.scala | 15 +- .../campudus/tableaux/router/BaseRouter.scala | 4 + .../tableaux/router/TableauxRouter.scala | 6 +- .../CreateHistoryChangeDetectionTest.scala | 215 ++++++++++-------- 6 files changed, 152 insertions(+), 113 deletions(-) diff --git a/src/main/resources/swagger.json b/src/main/resources/swagger.json index 66d30ca7..aa40caab 100644 --- a/src/main/resources/swagger.json +++ b/src/main/resources/swagger.json @@ -1065,6 +1065,9 @@ "parameters": [ { "$ref": "#/parameters/value" + }, + { + "$ref": "#/parameters/forceHistoryOpt" } ], "responses": { @@ -1086,6 +1089,9 @@ "parameters": [ { "$ref": "#/parameters/value" + }, + { + "$ref": "#/parameters/forceHistoryOpt" } ], "responses": { @@ -1107,6 +1113,9 @@ "parameters": [ { "$ref": "#/parameters/value" + }, + { + "$ref": "#/parameters/forceHistoryOpt" } ], "responses": { @@ -3825,6 +3834,14 @@ } } }, + "forceHistoryOpt": { + "name": "forceHistory", + "description": "If set to true, a history entry will be created even if the value did not change.", + "in": "query", + "required": false, + "type": "boolean", + "default": false + }, "fileuuid": { "name": "fileuuid", "in": "path", diff --git a/src/main/scala/com/campudus/tableaux/controller/TableauxController.scala b/src/main/scala/com/campudus/tableaux/controller/TableauxController.scala index 4f59bd7a..28e93cf2 100644 --- a/src/main/scala/com/campudus/tableaux/controller/TableauxController.scala +++ b/src/main/scala/com/campudus/tableaux/controller/TableauxController.scala @@ -458,18 +458,18 @@ class TableauxController( } yield filled } - def replaceCellValue[A](tableId: TableId, columnId: ColumnId, rowId: RowId, value: A)( + def replaceCellValue[A](tableId: TableId, columnId: ColumnId, rowId: RowId, value: A, forceHistory: Boolean = false)( implicit user: TableauxUser ): Future[Cell[_]] = { checkArguments(greaterZero(tableId), greaterZero(columnId), greaterZero(rowId)) logger.info(s"replaceCellValue $tableId $columnId $rowId $value") for { table <- repository.retrieveTable(tableId) - filled <- repository.replaceCellValue(table, columnId, rowId, value) + filled <- repository.replaceCellValue(table, columnId, rowId, value, forceHistory) } yield filled } - def updateCellValue[A](tableId: TableId, columnId: ColumnId, rowId: RowId, value: A)( + def updateCellValue[A](tableId: TableId, columnId: ColumnId, rowId: RowId, value: A, forceHistory: Boolean = false)( implicit user: TableauxUser ): Future[Cell[_]] = { checkArguments(greaterZero(tableId), greaterZero(columnId), greaterZero(rowId)) @@ -477,7 +477,7 @@ class TableauxController( for { table <- repository.retrieveTable(tableId) - updated <- repository.updateCellValue(table, columnId, rowId, value) + updated <- repository.updateCellValue(table, columnId, rowId, value, forceHistory) } yield updated } diff --git a/src/main/scala/com/campudus/tableaux/database/model/TableauxModel.scala b/src/main/scala/com/campudus/tableaux/database/model/TableauxModel.scala index e7021a30..9105540d 100644 --- a/src/main/scala/com/campudus/tableaux/database/model/TableauxModel.scala +++ b/src/main/scala/com/campudus/tableaux/database/model/TableauxModel.scala @@ -347,7 +347,7 @@ class TableauxModel( { case (future, (table, column, id)) => { future.flatMap(_ => { - updateOrReplaceValue(table, column.id, id, replacingRowId, false, t) + updateOrReplaceValue(table, column.id, id, replacingRowId, false, maybeTransaction = t) }).map(_ => ()) } } @@ -666,6 +666,7 @@ class TableauxModel( rowId: RowId, value: A, replace: Boolean = false, + forceHistory: Boolean = false, maybeTransaction: Option[DbTransaction] = None )(implicit user: TableauxUser): Future[Cell[_]] = { for { @@ -704,8 +705,10 @@ class TableauxModel( _ <- invalidateCellAndDependentColumns(column, rowId) newCell <- retrieveCell(column, rowId, true) + shouldCreateHistory = forceHistory || hasCellChanged(oldCell, newCell) + _ <- - if (hasCellChanged(oldCell, newCell)) { + if (shouldCreateHistory) { for { _ <- createHistoryModel.createCellsInit(table, rowId, oldCell, Seq((column, value))) _ <- createHistoryModel.createCells(table, rowId, Seq((column, value))) @@ -716,15 +719,15 @@ class TableauxModel( } yield newCell } - def updateCellValue[A](table: Table, columnId: ColumnId, rowId: RowId, value: A)( + def updateCellValue[A](table: Table, columnId: ColumnId, rowId: RowId, value: A, forceHistory: Boolean = false)( implicit user: TableauxUser ): Future[Cell[_]] = - updateOrReplaceValue(table, columnId, rowId, value) + updateOrReplaceValue(table, columnId, rowId, value, forceHistory = forceHistory) - def replaceCellValue[A](table: Table, columnId: ColumnId, rowId: RowId, value: A)( + def replaceCellValue[A](table: Table, columnId: ColumnId, rowId: RowId, value: A, forceHistory: Boolean = false)( implicit user: TableauxUser ): Future[Cell[_]] = - updateOrReplaceValue(table, columnId, rowId, value, replace = true) + updateOrReplaceValue(table, columnId, rowId, value, replace = true, forceHistory = true) def clearCellValue(table: Table, columnId: ColumnId, rowId: RowId)( implicit user: TableauxUser diff --git a/src/main/scala/com/campudus/tableaux/router/BaseRouter.scala b/src/main/scala/com/campudus/tableaux/router/BaseRouter.scala index a581933a..e2242ad1 100644 --- a/src/main/scala/com/campudus/tableaux/router/BaseRouter.scala +++ b/src/main/scala/com/campudus/tableaux/router/BaseRouter.scala @@ -158,6 +158,10 @@ trait BaseRouter extends VertxAccess { context.request().getParam(name).map(_.toBoolean) } + def getBoolQuery(name: String, context: RoutingContext): Option[Boolean] = { + context.queryParams().get(name).map(_.toBoolean) + } + def getStringParam(name: String, context: RoutingContext): Option[String] = { context.request().getParam(name) } diff --git a/src/main/scala/com/campudus/tableaux/router/TableauxRouter.scala b/src/main/scala/com/campudus/tableaux/router/TableauxRouter.scala index 095ea782..daf42139 100644 --- a/src/main/scala/com/campudus/tableaux/router/TableauxRouter.scala +++ b/src/main/scala/com/campudus/tableaux/router/TableauxRouter.scala @@ -665,6 +665,7 @@ class TableauxRouter(override val config: TableauxConfig, val controller: Tablea */ private def updateCell(context: RoutingContext): Unit = { implicit val user = TableauxUser(context) + val forceHistory = getBoolQuery("forceHistory", context).getOrElse(false) for { tableId <- getTableId(context) columnId <- getColumnId(context) @@ -675,7 +676,7 @@ class TableauxRouter(override val config: TableauxConfig, val controller: Tablea asyncGetReply { val json = getJson(context) for { - updated <- controller.updateCellValue(tableId, columnId, rowId, json.getValue("value")) + updated <- controller.updateCellValue(tableId, columnId, rowId, json.getValue("value"), forceHistory) } yield updated } ) @@ -687,6 +688,7 @@ class TableauxRouter(override val config: TableauxConfig, val controller: Tablea */ private def replaceCell(context: RoutingContext): Unit = { implicit val user = TableauxUser(context) + val forceHistory = getBoolQuery("forceHistory", context).getOrElse(false) for { tableId <- getTableId(context) columnId <- getColumnId(context) @@ -699,7 +701,7 @@ class TableauxRouter(override val config: TableauxConfig, val controller: Tablea for { updated <- if (json.containsKey("value")) { - controller.replaceCellValue(tableId, columnId, rowId, json.getValue("value")) + controller.replaceCellValue(tableId, columnId, rowId, json.getValue("value"), forceHistory) } else { Future.failed(InvalidJsonException("request must contain a value", "value_is_missing")) } diff --git a/src/test/scala/com/campudus/tableaux/api/content/CreateHistoryChangeDetectionTest.scala b/src/test/scala/com/campudus/tableaux/api/content/CreateHistoryChangeDetectionTest.scala index 0fa2e31e..273bc629 100644 --- a/src/test/scala/com/campudus/tableaux/api/content/CreateHistoryChangeDetectionTest.scala +++ b/src/test/scala/com/campudus/tableaux/api/content/CreateHistoryChangeDetectionTest.scala @@ -22,107 +22,110 @@ import org.skyscreamer.jsonassert.{JSONAssert, JSONCompareMode} class CreateHistoryChangeDetectionTest extends LinkTestBase with TestHelper { @Test - def changeSimpleValueTwice_changeACellOnlyOnce(implicit c: TestContext): Unit = { - okTest { - val newValue = Json.obj("value" -> "a fix value") - - for { - _ <- createEmptyDefaultTable() - _ <- sendRequest("POST", "/tables/1/rows") - _ <- sendRequest("POST", "/tables/1/columns/1/rows/1", newValue) - _ <- sendRequest("POST", "/tables/1/columns/1/rows/1", newValue) - rows <- sendRequest("GET", "/tables/1/columns/1/rows/1/history?historyType=cell").map(toRowsArray) - } yield { - println(s"rows: $rows") - assertEquals(1, rows.size()) - } + def changeSimpleValueTwice_changeACellOnlyOnce(implicit c: TestContext): Unit = okTest { + val newValue = Json.obj("value" -> "a fix value") + + for { + _ <- createEmptyDefaultTable() + _ <- sendRequest("POST", "/tables/1/rows") + _ <- sendRequest("POST", "/tables/1/columns/1/rows/1", newValue) + _ <- sendRequest("POST", "/tables/1/columns/1/rows/1", newValue) + rows <- sendRequest("GET", "/tables/1/columns/1/rows/1/history?historyType=cell").map(toRowsArray) + } yield { + assertEquals(1, rows.size()) } } @Test - def changeMultilanguageValueTwice_text(implicit c: TestContext): Unit = { - okTest { - val newValue = Json.obj("value" -> Json.obj("de-DE" -> "a fix value")) - - for { - _ <- createTableWithMultilanguageColumns("history test") - _ <- sendRequest("POST", "/tables/1/rows") - _ <- sendRequest("PATCH", "/tables/1/columns/1/rows/1", newValue) - _ <- sendRequest("PATCH", "/tables/1/columns/1/rows/1", newValue) - rows <- sendRequest("GET", "/tables/1/columns/1/rows/1/history?historyType=cell").map(toRowsArray) - } yield { - assertEquals(1, rows.size()) - } + def changeSimpleValueTwice_withForceHistory(implicit c: TestContext): Unit = okTest { + val newValue = Json.obj("value" -> "a fix value") + + for { + _ <- createEmptyDefaultTable() + _ <- sendRequest("POST", "/tables/1/rows") + _ <- sendRequest("POST", "/tables/1/columns/1/rows/1", newValue) + _ <- sendRequest("POST", "/tables/1/columns/1/rows/1?forceHistory=true", newValue) + rows <- sendRequest("GET", "/tables/1/columns/1/rows/1/history?historyType=cell").map(toRowsArray) + } yield { + assertEquals(2, rows.size()) } } @Test - def changeMultilanguageValueTwice_boolean(implicit c: TestContext): Unit = { - okTest { - val newValue = Json.obj("value" -> Json.obj("de-DE" -> true)) - - for { - _ <- createTableWithMultilanguageColumns("history test") - _ <- sendRequest("POST", "/tables/1/rows") - // Booleans always gets a initial history entry on first change, so +1 history row - _ <- sendRequest("PATCH", "/tables/1/columns/2/rows/1", newValue) - _ <- sendRequest("PATCH", "/tables/1/columns/2/rows/1", newValue) - rows <- sendRequest("GET", "/tables/1/columns/2/rows/1/history?historyType=cell").map(toRowsArray) - } yield { - assertEquals(2, rows.size()) - } + def changeMultilanguageValueTwice_text(implicit c: TestContext): Unit = okTest { + val newValue = Json.obj("value" -> Json.obj("de-DE" -> "a fix value")) + + for { + _ <- createTableWithMultilanguageColumns("history test") + _ <- sendRequest("POST", "/tables/1/rows") + _ <- sendRequest("PATCH", "/tables/1/columns/1/rows/1", newValue) + _ <- sendRequest("PATCH", "/tables/1/columns/1/rows/1", newValue) + rows <- sendRequest("GET", "/tables/1/columns/1/rows/1/history?historyType=cell").map(toRowsArray) + } yield { + assertEquals(1, rows.size()) } + } @Test - def changeMultilanguageValueTwice_numeric(implicit c: TestContext): Unit = { - okTest { - val newValue = Json.obj("value" -> Json.obj("de-DE" -> 42)) - - for { - _ <- createTableWithMultilanguageColumns("history test") - _ <- sendRequest("POST", "/tables/1/rows") - _ <- sendRequest("PATCH", "/tables/1/columns/3/rows/1", newValue) - _ <- sendRequest("PATCH", "/tables/1/columns/3/rows/1", newValue) - rows <- sendRequest("GET", "/tables/1/columns/3/rows/1/history?historyType=cell").map(toRowsArray) - } yield { - assertEquals(1, rows.size()) - } + def changeMultilanguageValueTwice_boolean(implicit c: TestContext): Unit = okTest { + val newValue = Json.obj("value" -> Json.obj("de-DE" -> true)) + + for { + _ <- createTableWithMultilanguageColumns("history test") + _ <- sendRequest("POST", "/tables/1/rows") + // Booleans always gets a initial history entry on first change, so +1 history row + _ <- sendRequest("PATCH", "/tables/1/columns/2/rows/1", newValue) + _ <- sendRequest("PATCH", "/tables/1/columns/2/rows/1", newValue) + rows <- sendRequest("GET", "/tables/1/columns/2/rows/1/history?historyType=cell").map(toRowsArray) + } yield { + assertEquals(2, rows.size()) } } @Test - def changeMultilanguageValueTwice_datetime(implicit c: TestContext): Unit = { - okTest { - val newValue = Json.obj("value" -> Json.obj("de-DE" -> "2019-01-18T00:00:00.000Z")) - - for { - _ <- createTableWithMultilanguageColumns("history test") - _ <- sendRequest("POST", "/tables/1/rows") - _ <- sendRequest("PATCH", "/tables/1/columns/7/rows/1", newValue) - _ <- sendRequest("PATCH", "/tables/1/columns/7/rows/1", newValue) - rows <- sendRequest("GET", "/tables/1/columns/7/rows/1/history?historyType=cell").map(toRowsArray) - } yield { - assertEquals(1, rows.size()) - } + def changeMultilanguageValueTwice_numeric(implicit c: TestContext): Unit = okTest { + val newValue = Json.obj("value" -> Json.obj("de-DE" -> 42)) + + for { + _ <- createTableWithMultilanguageColumns("history test") + _ <- sendRequest("POST", "/tables/1/rows") + _ <- sendRequest("PATCH", "/tables/1/columns/3/rows/1", newValue) + _ <- sendRequest("PATCH", "/tables/1/columns/3/rows/1", newValue) + rows <- sendRequest("GET", "/tables/1/columns/3/rows/1/history?historyType=cell").map(toRowsArray) + } yield { + assertEquals(1, rows.size()) } } @Test - def changeMultilanguageValueTwice_currency(implicit c: TestContext): Unit = { - okTest { - val newValue = Json.obj("value" -> Json.obj("DE" -> 2999.99)) - val multiCountryCurrencyColumn = MultiCountry(CurrencyCol("currency-column"), Seq("DE", "GB")) - - for { - _ <- createSimpleTableWithCell("table1", multiCountryCurrencyColumn) - _ <- sendRequest("POST", "/tables/1/rows") - _ <- sendRequest("PATCH", "/tables/1/columns/1/rows/1", newValue) - _ <- sendRequest("PATCH", "/tables/1/columns/1/rows/1", newValue) - rows <- sendRequest("GET", "/tables/1/columns/1/rows/1/history?historyType=cell").map(toRowsArray) - } yield { - assertEquals(1, rows.size()) - } + def changeMultilanguageValueTwice_datetime(implicit c: TestContext): Unit = okTest { + val newValue = Json.obj("value" -> Json.obj("de-DE" -> "2019-01-18T00:00:00.000Z")) + + for { + _ <- createTableWithMultilanguageColumns("history test") + _ <- sendRequest("POST", "/tables/1/rows") + _ <- sendRequest("PATCH", "/tables/1/columns/7/rows/1", newValue) + _ <- sendRequest("PATCH", "/tables/1/columns/7/rows/1", newValue) + rows <- sendRequest("GET", "/tables/1/columns/7/rows/1/history?historyType=cell").map(toRowsArray) + } yield { + assertEquals(1, rows.size()) + } + } + + @Test + def changeMultilanguageValueTwice_currency(implicit c: TestContext): Unit = okTest { + val newValue = Json.obj("value" -> Json.obj("DE" -> 2999.99)) + val multiCountryCurrencyColumn = MultiCountry(CurrencyCol("currency-column"), Seq("DE", "GB")) + + for { + _ <- createSimpleTableWithCell("table1", multiCountryCurrencyColumn) + _ <- sendRequest("POST", "/tables/1/rows") + _ <- sendRequest("PATCH", "/tables/1/columns/1/rows/1", newValue) + _ <- sendRequest("PATCH", "/tables/1/columns/1/rows/1", newValue) + rows <- sendRequest("GET", "/tables/1/columns/1/rows/1/history?historyType=cell").map(toRowsArray) + } yield { + assertEquals(1, rows.size()) } } @@ -132,8 +135,6 @@ class CreateHistoryChangeDetectionTest extends LinkTestBase with TestHelper { for { linkColumnId <- setupTwoTablesWithEmptyLinks() - - _ = println(s"linkColumnId: $linkColumnId") _ <- sendRequest("PUT", s"/tables/1/columns/$linkColumnId/rows/1", twoLinks) _ <- sendRequest("PUT", s"/tables/1/columns/$linkColumnId/rows/1", twoLinks) rows <- sendRequest("GET", s"/tables/1/columns/$linkColumnId/rows/1/history?historyType=cell").map(toRowsArray) @@ -142,6 +143,20 @@ class CreateHistoryChangeDetectionTest extends LinkTestBase with TestHelper { } } + @Test + def changeLinkValueTwice_withForceHistory(implicit c: TestContext): Unit = okTest { + val twoLinks = Json.obj("value" -> Json.obj("values" -> Json.arr(1, 2))) + + for { + linkColumnId <- setupTwoTablesWithEmptyLinks() + _ <- sendRequest("PUT", s"/tables/1/columns/$linkColumnId/rows/1", twoLinks) + _ <- sendRequest("PUT", s"/tables/1/columns/$linkColumnId/rows/1?forceHistory=true", twoLinks) + rows <- sendRequest("GET", s"/tables/1/columns/$linkColumnId/rows/1/history?historyType=cell").map(toRowsArray) + } yield { + assertEquals(2, rows.size()) + } + } + @Test def changeLinkValueTwice_singleLanguageMultiIdentifiers(implicit c: TestContext): Unit = okTest { val twoLinks = Json.obj("value" -> Json.obj("values" -> Json.arr(1, 2))) @@ -179,24 +194,22 @@ class CreateHistoryChangeDetectionTest extends LinkTestBase with TestHelper { } @Test - def changeLink_addTwoLinksAtOnce(implicit c: TestContext): Unit = { - okTest { - val putLink = Json.obj("value" -> Json.obj("values" -> Json.arr(4, 5))) - - for { - _ <- setupTwoTablesWithEmptyLinks() - - _ <- sendRequest("PUT", s"/tables/1/columns/3/rows/1", putLink) - _ <- sendRequest("PUT", s"/tables/1/columns/3/rows/1", putLink) - - history <- sendRequest("GET", "/tables/1/columns/3/rows/1/history?historyType=cell").map(toRowsArray) - targetHistory1 <- sendRequest("GET", "/tables/2/columns/3/rows/4/history?historyType=cell").map(toRowsArray) - targetHistory2 <- sendRequest("GET", "/tables/2/columns/3/rows/5/history?historyType=cell").map(toRowsArray) - } yield { - assertEquals(1, history.size()) - assertEquals(1, targetHistory1.size()) - assertEquals(1, targetHistory2.size()) - } + def changeLink_addTwoLinksAtOnce(implicit c: TestContext): Unit = okTest { + val putLink = Json.obj("value" -> Json.obj("values" -> Json.arr(4, 5))) + + for { + _ <- setupTwoTablesWithEmptyLinks() + + _ <- sendRequest("PUT", s"/tables/1/columns/3/rows/1", putLink) + _ <- sendRequest("PUT", s"/tables/1/columns/3/rows/1", putLink) + + history <- sendRequest("GET", "/tables/1/columns/3/rows/1/history?historyType=cell").map(toRowsArray) + targetHistory1 <- sendRequest("GET", "/tables/2/columns/3/rows/4/history?historyType=cell").map(toRowsArray) + targetHistory2 <- sendRequest("GET", "/tables/2/columns/3/rows/5/history?historyType=cell").map(toRowsArray) + } yield { + assertEquals(1, history.size()) + assertEquals(1, targetHistory1.size()) + assertEquals(1, targetHistory2.size()) } } } From ec2accca72402a7f0926f2ee8a8bf09a58cd50fb Mon Sep 17 00:00:00 2001 From: Manfred Zingl Date: Tue, 24 Oct 2023 17:56:36 +0200 Subject: [PATCH 8/9] fix historyCellInit --- .../controller/TableauxController.scala | 3 +- .../database/model/HistoryModel.scala | 130 +++++++++--------- .../database/model/TableauxModel.scala | 32 ++--- 3 files changed, 76 insertions(+), 89 deletions(-) diff --git a/src/main/scala/com/campudus/tableaux/controller/TableauxController.scala b/src/main/scala/com/campudus/tableaux/controller/TableauxController.scala index 28e93cf2..0143d2f8 100644 --- a/src/main/scala/com/campudus/tableaux/controller/TableauxController.scala +++ b/src/main/scala/com/campudus/tableaux/controller/TableauxController.scala @@ -506,8 +506,7 @@ class TableauxController( _ <- roleModel.checkAuthorization(EditCellValue, ComparisonObjects(table, column)) - oldCell <- repository.retrieveCell(table, column.id, rowId, true) - _ <- repository.createHistoryModel.createCellsInit(table, rowId, oldCell, Seq((column, uuid))) + _ <- repository.createHistoryModel.createCellsInit(table, rowId, Seq((column, uuid))) _ <- repository.attachmentModel.delete(Attachment(tableId, columnId, rowId, UUID.fromString(uuid), None)) _ <- CacheClient(this).invalidateCellValue(tableId, columnId, rowId) _ <- repository.createHistoryModel.createCells(table, rowId, Seq((column, uuid))) diff --git a/src/main/scala/com/campudus/tableaux/database/model/HistoryModel.scala b/src/main/scala/com/campudus/tableaux/database/model/HistoryModel.scala index a18b694f..a42a59dd 100644 --- a/src/main/scala/com/campudus/tableaux/database/model/HistoryModel.scala +++ b/src/main/scala/com/campudus/tableaux/database/model/HistoryModel.scala @@ -141,42 +141,18 @@ case class CreateHistoryModel(tableauxModel: TableauxModel, connection: Database val tableModel = new TableModel(connection) val attachmentModel = AttachmentModel(connection) - private def toInitCellValueOption( - column: ColumnType[_], - cell: Cell[_], - langtagCountryOpt: Option[String] = None - ): Option[Any] = { - Option(cell.value) match { - case Some(v) => - column match { - case MultiLanguageColumn(_) => - val rawValue = cell.getJson.getJsonObject("value") - column match { - case _: BooleanColumn => Option(rawValue.getBoolean(langtagCountryOpt.getOrElse(""), false)) - case _ => Option(rawValue.getValue(langtagCountryOpt.getOrElse(""))) - } - case _: SimpleValueColumn[_] => Some(v) - } - case _ => None - } - } - - private def toInitLinkIds(column: LinkColumn, cell: Cell[_]): Seq[RowId] = { - cell.getJson - .getJsonArray("value") - .asScala - .map(_.asInstanceOf[JsonObject]) - .map(_.getLong("id").longValue()) - .toSeq - } - private def retrieveCurrentLinkIds(table: Table, column: LinkColumn, rowId: RowId)( implicit user: TableauxUser ): Future[Seq[RowId]] = { for { cell <- tableauxModel.retrieveCell(table, column.id, rowId, isInternalCall = true) } yield { - toInitLinkIds(column, cell) + cell.getJson + .getJsonArray("value") + .asScala + .map(_.asInstanceOf[JsonObject]) + .map(_.getLong("id").longValue()) + .toSeq } } @@ -538,7 +514,7 @@ case class CreateHistoryModel(tableauxModel: TableauxModel, connection: Database * If we didn't call this method on any cell change/deletion the previously valid value wouldn't be logged in a * history table. */ - def createCellsInit(table: Table, rowId: RowId, oldCell: Cell[_], values: Seq[(ColumnType[_], _)])( + def createCellsInit(table: Table, rowId: RowId, values: Seq[(ColumnType[_], _)])( implicit user: TableauxUser ): Future[Unit] = { val columns = values.map({ case (col: ColumnType[_], _) => col }) @@ -556,13 +532,10 @@ case class CreateHistoryModel(tableauxModel: TableauxModel, connection: Database } yield (langtagSeq, column) for { - _ <- if (simples.isEmpty) Future.successful(()) else createSimpleInit(table, rowId, oldCell, simples) - _ <- - if (multis.isEmpty) Future.successful(()) else createTranslationInit(table, rowId, oldCell, langtagColumns) - _ <- if (links.isEmpty) Future.successful(()) else createLinksInit(table, rowId, oldCell, links) - _ <- - if (attachments.isEmpty) Future.successful(()) - else createAttachmentsInit(table, rowId, oldCell, attachments) + _ <- if (simples.isEmpty) Future.successful(()) else createSimpleInit(table, rowId, simples) + _ <- if (multis.isEmpty) Future.successful(()) else createTranslationInit(table, rowId, langtagColumns) + _ <- if (links.isEmpty) Future.successful(()) else createLinksInit(table, rowId, links) + _ <- if (attachments.isEmpty) Future.successful(()) else createAttachmentsInit(table, rowId, attachments) } yield () } } @@ -573,7 +546,7 @@ case class CreateHistoryModel(tableauxModel: TableauxModel, connection: Database * If we didn't call this method on any cell change/deletion the previously valid value wouldn't be logged in a * history table. */ - def createClearCellInit(table: Table, rowId: RowId, oldCell: Cell[_], columns: Seq[ColumnType[_]])( + def createClearCellInit(table: Table, rowId: RowId, columns: Seq[ColumnType[_]])( implicit user: TableauxUser ): Future[Unit] = { val (simples, multis, links, attachments) = ColumnType.splitIntoTypes(columns) @@ -585,14 +558,14 @@ case class CreateHistoryModel(tableauxModel: TableauxModel, connection: Database } yield (langtag, column) for { - _ <- if (simples.isEmpty) Future.successful(()) else createSimpleInit(table, rowId, oldCell, simples) - _ <- if (multis.isEmpty) Future.successful(()) else createTranslationInit(table, rowId, oldCell, langtagColumns) - _ <- if (links.isEmpty) Future.successful(()) else createLinksInit(table, rowId, oldCell, links) - _ <- if (attachments.isEmpty) Future.successful(()) else createAttachmentsInit(table, rowId, oldCell, attachments) + _ <- if (simples.isEmpty) Future.successful(()) else createSimpleInit(table, rowId, simples) + _ <- if (multis.isEmpty) Future.successful(()) else createTranslationInit(table, rowId, langtagColumns) + _ <- if (links.isEmpty) Future.successful(()) else createLinksInit(table, rowId, links) + _ <- if (attachments.isEmpty) Future.successful(()) else createAttachmentsInit(table, rowId, attachments) } yield () } - def createAttachmentsInit(table: Table, rowId: RowId, oldCell: Cell[_], columns: Seq[AttachmentColumn])( + def createAttachmentsInit(table: Table, rowId: RowId, columns: Seq[AttachmentColumn])( implicit user: TableauxUser ): Future[Seq[Unit]] = { Future.sequence(columns.map({ column => @@ -615,17 +588,20 @@ case class CreateHistoryModel(tableauxModel: TableauxModel, connection: Database })) } - def createLinksInit(table: Table, rowId: RowId, oldCell: Cell[_], links: Seq[LinkColumn])( + def createLinksInit(table: Table, rowId: RowId, links: Seq[LinkColumn])( implicit user: TableauxUser ): Future[Seq[Unit]] = { def createIfNotEmpty(linkColumn: LinkColumn): Future[Unit] = { - val linkIds = toInitLinkIds(linkColumn, oldCell) - if (linkIds.nonEmpty) { - createLinks(table, rowId, Seq((linkColumn, linkIds)), allowRecursion = true).map(_ => ()) - } else { - Future.successful(()) - } + for { + linkIds <- retrieveCurrentLinkIds(table, linkColumn, rowId) + _ <- + if (linkIds.nonEmpty) { + createLinks(table, rowId, Seq((linkColumn, linkIds)), allowRecursion = true) + } else { + Future.successful(()) + } + } yield () } Future.sequence(links.map({ linkColumn => @@ -636,16 +612,18 @@ case class CreateHistoryModel(tableauxModel: TableauxModel, connection: Database })) } - private def createSimpleInit(table: Table, rowId: RowId, oldCell: Cell[_], simples: Seq[SimpleValueColumn[_]])( + private def createSimpleInit(table: Table, rowId: RowId, simples: Seq[SimpleValueColumn[_]])( implicit user: TableauxUser ): Future[Seq[Unit]] = { def createIfNotEmpty(column: SimpleValueColumn[_]): Future[Unit] = { - val value = toInitCellValueOption(column, oldCell) - value match { - case Some(v) => createSimple(table, rowId, Seq((column, Option(v)))).map(_ => ()) - case None => Future.successful(()) - } + for { + value <- retrieveCellValue(table, column, rowId) + _ <- value match { + case Some(v) => createSimple(table, rowId, Seq((column, Option(v)))) + case None => Future.successful(()) + } + } yield () } Future.sequence(simples.map({ column => @@ -659,7 +637,6 @@ case class CreateHistoryModel(tableauxModel: TableauxModel, connection: Database private def createTranslationInit( table: Table, rowId: RowId, - oldCell: Cell[_], langtagColumns: Seq[(Seq[String], SimpleValueColumn[_])] )(implicit user: TableauxUser): Future[Seq[Unit]] = { @@ -674,11 +651,13 @@ case class CreateHistoryModel(tableauxModel: TableauxModel, connection: Database .toSeq def createIfNotEmpty(column: SimpleValueColumn[_], langtag: String): Future[Unit] = { - val value = toInitCellValueOption(column, oldCell, Option(langtag)) - value match { - case Some(v) => createTranslation(table, rowId, Seq((column, Map(langtag -> Option(v))))).map(_ => ()) - case None => Future.successful(()) - } + for { + value <- retrieveCellValue(table, column, rowId, Option(langtag)) + _ <- value match { + case Some(v) => createTranslation(table, rowId, Seq((column, Map(langtag -> Option(v))))) + case None => Future.successful(()) + } + } yield () } Future.sequence( @@ -712,6 +691,31 @@ case class CreateHistoryModel(tableauxModel: TableauxModel, connection: Database ) } + private def retrieveCellValue( + table: Table, + column: ColumnType[_], + rowId: RowId, + langtagCountryOpt: Option[String] = None + )(implicit user: TableauxUser): Future[Option[Any]] = { + for { + cell <- tableauxModel.retrieveCell(table, column.id, rowId, isInternalCall = true) + } yield { + Option(cell.value) match { + case Some(v) => + column match { + case MultiLanguageColumn(_) => + val rawValue = cell.getJson.getJsonObject("value") + column match { + case _: BooleanColumn => Option(rawValue.getBoolean(langtagCountryOpt.getOrElse(""), false)) + case _ => Option(rawValue.getValue(langtagCountryOpt.getOrElse(""))) + } + case _: SimpleValueColumn[_] => Some(v) + } + case _ => None + } + } + } + /** * Writes empty back link history for the linked rows that are going to be deleted. The difference of the existing * linked foreign row IDs and the linked foreign row IDs after the change are going to be cleared. diff --git a/src/main/scala/com/campudus/tableaux/database/model/TableauxModel.scala b/src/main/scala/com/campudus/tableaux/database/model/TableauxModel.scala index 9105540d..3d0163d6 100644 --- a/src/main/scala/com/campudus/tableaux/database/model/TableauxModel.scala +++ b/src/main/scala/com/campudus/tableaux/database/model/TableauxModel.scala @@ -306,16 +306,7 @@ class TableauxModel( .collect({ case c: LinkColumn if !c.linkDirection.constraint.deleteCascade => c }) .map(col => (col, 0)) - _ <- Future.sequence( - columns.map({ - case c: ColumnType[_] => { - for { - cell <- retrieveCell(c, rowId, true) - _ <- createHistoryModel.createCellsInit(table, rowId, cell, Seq.empty) - } yield () - } - }) - ) + _ <- createHistoryModel.createCellsInit(table, rowId, columnValueLinks) linkList <- retrieveDependentCells(table, rowId) @@ -540,8 +531,7 @@ class TableauxModel( _ <- column match { case linkColumn: LinkColumn => { for { - oldCell <- retrieveCell(column, rowId, true) - _ <- createHistoryModel.createCellsInit(table, rowId, oldCell, Seq((column, Seq(toId)))) + _ <- createHistoryModel.createCellsInit(table, rowId, Seq((column, Seq(toId)))) _ <- updateRowModel.deleteLink(table, linkColumn, rowId, toId, deleteRow) _ <- invalidateCellAndDependentColumns(column, rowId) _ <- createHistoryModel.deleteLink(table, linkColumn, rowId, toId) @@ -568,8 +558,7 @@ class TableauxModel( _ <- column match { case linkColumn: LinkColumn => { for { - oldCell <- retrieveCell(column, rowId, true) - _ <- createHistoryModel.createCellsInit(table, rowId, oldCell, Seq((linkColumn, Seq(rowId)))) + _ <- createHistoryModel.createCellsInit(table, rowId, Seq((linkColumn, Seq(rowId)))) _ <- updateRowModel.updateLinkOrder(table, linkColumn, rowId, toId, locationType) _ <- invalidateCellAndDependentColumns(column, rowId) _ <- createHistoryModel.updateLinks(table, linkColumn, Seq(rowId)) @@ -656,9 +645,7 @@ class TableauxModel( } } - private def hasCellChanged(oldCell: Cell[_], newCell: Cell[_]): Boolean = { - oldCell.value != newCell.value - } + private def hasCellChanged(oldCell: Cell[_], newCell: Cell[_]): Boolean = oldCell.value != newCell.value private def updateOrReplaceValue[A]( table: Table, @@ -690,6 +677,7 @@ class TableauxModel( } oldCell <- retrieveCell(column, rowId, true) + _ <- createHistoryModel.createCellsInit(table, rowId, Seq((column, value))) _ <- if (replace) { @@ -709,10 +697,7 @@ class TableauxModel( _ <- if (shouldCreateHistory) { - for { - _ <- createHistoryModel.createCellsInit(table, rowId, oldCell, Seq((column, value))) - _ <- createHistoryModel.createCells(table, rowId, Seq((column, value))) - } yield () + createHistoryModel.createCells(table, rowId, Seq((column, value))) } else { Future.successful(()) } @@ -727,7 +712,7 @@ class TableauxModel( def replaceCellValue[A](table: Table, columnId: ColumnId, rowId: RowId, value: A, forceHistory: Boolean = false)( implicit user: TableauxUser ): Future[Cell[_]] = - updateOrReplaceValue(table, columnId, rowId, value, replace = true, forceHistory = true) + updateOrReplaceValue(table, columnId, rowId, value, replace = true, forceHistory = forceHistory) def clearCellValue(table: Table, columnId: ColumnId, rowId: RowId)( implicit user: TableauxUser @@ -745,8 +730,7 @@ class TableauxModel( roleModel.checkAuthorization(EditCellValue, ComparisonObjects(table, column)) } - oldCell <- retrieveCell(column, rowId, true) - _ <- createHistoryModel.createClearCellInit(table, rowId, oldCell, Seq(column)) + _ <- createHistoryModel.createClearCellInit(table, rowId, Seq(column)) _ <- createHistoryModel.createClearCell(table, rowId, Seq(column)) _ <- updateRowModel.clearRow(table, rowId, Seq(column), deleteRow) _ <- invalidateCellAndDependentColumns(column, rowId) From 0142b9dcfa7541e445cdeb7a3d8fdd3257b60e2a Mon Sep 17 00:00:00 2001 From: Manfred Zingl Date: Tue, 24 Oct 2023 18:12:15 +0200 Subject: [PATCH 9/9] fix test --- .../com/campudus/tableaux/api/content/CreateHistoryTest.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/scala/com/campudus/tableaux/api/content/CreateHistoryTest.scala b/src/test/scala/com/campudus/tableaux/api/content/CreateHistoryTest.scala index 35f664d4..8be35aad 100644 --- a/src/test/scala/com/campudus/tableaux/api/content/CreateHistoryTest.scala +++ b/src/test/scala/com/campudus/tableaux/api/content/CreateHistoryTest.scala @@ -1637,7 +1637,7 @@ class CreateHistoryCompatibilityTest extends LinkTestBase with TestHelper { // Booleans always gets a initial history entry on first change val expectedInitialLinks = """{ "value": true} """ - val expectedAfterPost1 = """{ "value": true }""" + val expectedAfterPost1 = """{ "value": false }""" val booleanColumn = s"""{"columns": [{"kind": "boolean", "name": "Boolean Column", "languageType": "neutral"} ] }"""