diff --git a/image-api/src/main/resources/no/ndla/imageapi/db/migration/V29__AddImageEditorsTable.sql b/image-api/src/main/resources/no/ndla/imageapi/db/migration/V29__AddImageEditorsTable.sql new file mode 100644 index 0000000000..31d3131a63 --- /dev/null +++ b/image-api/src/main/resources/no/ndla/imageapi/db/migration/V29__AddImageEditorsTable.sql @@ -0,0 +1,30 @@ +create table image_editors +( + image_id bigint not null, + user_id text not null, + primary key (image_id, user_id) +); + +insert into image_editors (image_id, user_id) +select distinct note_users.image_id, note_users.user_id +from (select id as image_id, + notes ->> 'updatedBy' as user_id + from imagemetadata + cross join lateral jsonb_array_elements(metadata -> 'editorNotes') as notes) as note_users +where user_id is not null +on conflict do nothing; + +insert into image_editors (image_id, user_id) +select id, metadata ->> 'createdBy' +from imagemetadata +where metadata is not null + and metadata ->> 'createdBy' is not null +on conflict do nothing; + +insert into image_editors (image_id, user_id) +select id, metadata ->> 'updatedBy' +from imagemetadata +where metadata is not null + and metadata ->> 'updatedBy' is not null +on conflict do nothing; + diff --git a/image-api/src/main/scala/no/ndla/imageapi/repository/ImageRepository.scala b/image-api/src/main/scala/no/ndla/imageapi/repository/ImageRepository.scala index f64efd1988..ad9654e47b 100644 --- a/image-api/src/main/scala/no/ndla/imageapi/repository/ImageRepository.scala +++ b/image-api/src/main/scala/no/ndla/imageapi/repository/ImageRepository.scala @@ -47,9 +47,11 @@ class ImageRepository(using dbUtility: DBUtility, dbImageMetaInformation: DBImag dataObject.setType("jsonb") dataObject.setValue(CirceUtil.toJsonString(imageMeta)) - tsql"insert into imagemetadata(metadata) values ($dataObject)" - .updateAndReturnGeneratedKey() - .map(id => imageMeta.copy(id = Some(id))) + for { + id <- tsql"insert into imagemetadata(metadata) values ($dataObject)".updateAndReturnGeneratedKey() + inserted = imageMeta.copy(id = Some(id)) + tracked <- trackEditor(inserted) + } yield tracked def update(imageMetaInformation: ImageMetaInformation, id: Long)(implicit session: DBSession = dbUtility.autoSession @@ -58,17 +60,42 @@ class ImageRepository(using dbUtility: DBUtility, dbImageMetaInformation: DBImag val dataObject = new PGobject() dataObject.setType("jsonb") dataObject.setValue(json) - tsql"update imagemetadata set metadata = $dataObject where id = $id" - .update() - .map(_ => imageMetaInformation.copy(id = Some(id))) + for { + updated <- tsql"update imagemetadata set metadata = $dataObject where id = $id" + .update() + .map(_ => imageMetaInformation.copy(id = Some(id))) + tracked <- trackEditor(updated) + } yield tracked } - def delete(imageId: Long)(implicit session: DBSession = dbUtility.autoSession): Try[Int] = Try { - tsql"delete from imagemetadata where id = $imageId".update().get - }.flatMap { - case n if n < 1 => - Failure(new ImageNotFoundException(s"Image with id $imageId was not found, and could not be deleted.")) - case n => Success(n) + def getAllEditors(implicit session: DBSession = dbUtility.readOnlySession): Try[Seq[String]] = + tsql"select distinct user_id from image_editors".map(rs => rs.string("user_id")).runList() + + private def trackEditor(image: ImageMetaInformation)(implicit session: DBSession): Try[ImageMetaInformation] = { + image + .id + .map { id => + tsql""" + insert into image_editors (image_id, user_id) + values ($id, ${image.updatedBy}) + on conflict do nothing + """.update() + } + .getOrElse(Success(())) + .map(_ => image) + } + + def delete(imageId: Long)(implicit session: DBSession = dbUtility.autoSession): Try[Int] = { + for { + result <- tsql"delete from imagemetadata where id = $imageId" + .update() + .flatMap { + case n if n < 1 => + Failure(new ImageNotFoundException(s"Image with id $imageId was not found, and could not be deleted.")) + case n => Success(n) + } + _ <- tsql"delete from image_editors where image_id = $imageId".update() + } yield result } def minMaxId: Try[(Long, Long)] = dbUtility.readOnly { implicit session => diff --git a/image-api/src/test/scala/no/ndla/imageapi/repository/ImageRepositoryTest.scala b/image-api/src/test/scala/no/ndla/imageapi/repository/ImageRepositoryTest.scala index 141eb45364..23afc9917e 100644 --- a/image-api/src/test/scala/no/ndla/imageapi/repository/ImageRepositoryTest.scala +++ b/image-api/src/test/scala/no/ndla/imageapi/repository/ImageRepositoryTest.scala @@ -9,10 +9,11 @@ package no.ndla.imageapi.repository import java.net.Socket -import no.ndla.imageapi.model.domain.ImageTitle +import no.ndla.imageapi.model.domain.{EditorNote, ImageTitle} import no.ndla.imageapi.{ImageApiProperties, TestEnvironment, UnitSuite} import no.ndla.scalatestsuite.DatabaseIntegrationSuite import no.ndla.database.{DataSource, DBMigrator, DBUtility} +import no.ndla.common.model.NDLADate import scala.util.{Success, Try} import scalikejdbc.* @@ -36,7 +37,8 @@ class ImageRepositoryTest extends DatabaseIntegrationSuite with UnitSuite with T } def emptyTestDatabase: Boolean = dbUtility.writeSession(implicit session => { - sql"delete from imagemetadata;".execute()(using session) + sql"delete from image_editors".execute()(using session) + sql"delete from imagemetadata".execute()(using session) }) override def beforeAll(): Unit = { @@ -144,4 +146,24 @@ class ImageRepositoryTest extends DatabaseIntegrationSuite with UnitSuite with T repository.getImageFromFilePath(path2).get should be(Some(expected2)) } + test("That inserting and updating images updates the image_editors table") { + repository.getAllEditors.get should be(Nil) + + val image = TestData.bjorn.copy(id = None, createdBy = "creator-1", updatedBy = "editor-1", editorNotes = Seq.empty) + val inserted = repository.insert(image).failIfFailure + val id = inserted.id.get + + repository.getAllEditors.get.toSet should be(Set("editor-1")) + + val note = EditorNote(NDLADate.now(), "note-editor-1", "Some note") + val updated = inserted.copy(updatedBy = "note-editor-1", editorNotes = Seq(note)) + repository.update(updated, id).failIfFailure + + repository.getAllEditors.get.toSet should be(Set("editor-1", "note-editor-1")) + + repository.delete(id).failIfFailure + + repository.getAllEditors.get should be(Nil) + } + }