diff --git a/api/app/controllers/Changes.scala b/api/app/controllers/Changes.scala index e83d54a6b..85bc69b40 100644 --- a/api/app/controllers/Changes.scala +++ b/api/app/controllers/Changes.scala @@ -1,15 +1,18 @@ package controllers -import io.apibuilder.api.v0.models.json._ -import db.ChangesDao +import io.apibuilder.api.v0.models.json.* +import db.InternalChangesDao +import models.ChangesModel + import javax.inject.{Inject, Singleton} -import play.api.mvc._ -import play.api.libs.json._ +import play.api.mvc.* +import play.api.libs.json.* @Singleton class Changes @Inject() ( val apiBuilderControllerComponents: ApiBuilderControllerComponents, - changesDao: ChangesDao + changesDao: InternalChangesDao, + model: ChangesModel ) extends ApiBuilderController { def get( @@ -28,10 +31,10 @@ class Changes @Inject() ( fromVersion = from, toVersion = to, `type` = `type`, - limit = limit, + limit = Some(limit), offset = offset ) - Ok(Json.toJson(changes)) + Ok(Json.toJson(model.toModels(changes))) } } diff --git a/api/app/controllers/Versions.scala b/api/app/controllers/Versions.scala index 844e64cc9..1227c175e 100644 --- a/api/app/controllers/Versions.scala +++ b/api/app/controllers/Versions.scala @@ -32,7 +32,7 @@ class Versions @Inject() ( versionsDao.findAll( request.authorization, applicationGuid = Some(application.guid), - limit = limit, + limit = Some(limit), offset = offset ) }.getOrElse(Nil) diff --git a/api/app/db/ChangesDao.scala b/api/app/db/ChangesDao.scala deleted file mode 100644 index a541a1d31..000000000 --- a/api/app/db/ChangesDao.scala +++ /dev/null @@ -1,206 +0,0 @@ -package db - -import anorm.JodaParameterMetaData._ -import anorm._ -import io.apibuilder.api.v0.models._ -import io.apibuilder.common.v0.models.{Audit, Reference, ReferenceGuid} -import io.flow.postgresql.Query -import lib.VersionTag -import org.postgresql.util.PSQLException -import play.api.db._ - -import java.util.UUID -import javax.inject.{Inject, Singleton} -import scala.util.{Failure, Success, Try} - -@Singleton -class ChangesDao @Inject() ( - @NamedDatabase("default") db: Database -) { - private object DiffType { - val Breaking = "breaking" - val NonBreaking = "non_breaking" - } - - private val BaseQuery = Query( - s""" - select changes.guid, - changes.type, - changes.description, - changes.is_material, - changes.changed_at, - changes.changed_by_guid::uuid, - ${AuditsDao.queryCreationDefaultingUpdatedAt("changes")}, - applications.guid as application_guid, - applications.key as application_key, - organizations.guid as organization_guid, - organizations.key as organization_key, - from_version.guid::text as from_version_guid, - from_version.version as from_version_version, - to_version.guid::text as to_version_guid, - to_version.version as to_version_version, - users.nickname as changed_by_nickname - from changes - join applications on applications.guid = changes.application_guid and applications.deleted_at is null - join organizations on organizations.guid = applications.organization_guid and organizations.deleted_at is null - join versions from_version on from_version.guid = changes.from_version_guid - join versions to_version on to_version.guid = changes.to_version_guid - join users on users.guid = changes.changed_by_guid - """) - - private val InsertQuery = - """ - insert into changes - (guid, application_guid, from_version_guid, to_version_guid, type, description, is_material, changed_at, changed_by_guid, created_by_guid) - values - ({guid}::uuid, {application_guid}::uuid, {from_version_guid}::uuid, {to_version_guid}::uuid, {type}, {description}, {is_material}::boolean, {changed_at}, {changed_by_guid}::uuid, {created_by_guid}::uuid) - """ - - def upsert( - createdBy: User, - fromVersion: Version, - toVersion: Version, - differences: Seq[Diff] - ): Unit = { - assert( - fromVersion.guid != toVersion.guid, - "Versions must be different" - ) - - assert( - fromVersion.application.guid == toVersion.application.guid, - "Versions must belong to same application" - ) - - db.withTransaction { implicit c => - differences.map { - case d: DiffBreaking => (DiffType.Breaking, d) - case d: DiffNonBreaking => (DiffType.NonBreaking, d) - case DiffUndefinedType(desc) => sys.error(s"Unrecognized difference type: $desc") - }.distinct.foreach { - case (differenceType, diff) => { - Try( - SQL(InsertQuery).on( - "guid" -> UUID.randomUUID, - "application_guid" -> fromVersion.application.guid, - "from_version_guid" -> fromVersion.guid, - "to_version_guid" -> toVersion.guid, - "type" -> differenceType, - "description" -> diff.description, - "is_material" -> diff.isMaterial, - "changed_at" -> toVersion.audit.createdAt, - "changed_by_guid" -> toVersion.audit.createdBy.guid, - "created_by_guid" -> createdBy.guid - ).execute() - ) match { - case Success(_) => {} - case Failure(e) => e match { - case e: PSQLException => { - findAll( - Authorization.All, - fromVersionGuid = Some(fromVersion.guid), - toVersionGuid = Some(toVersion.guid), - description = Some(diff.description) - ).headOption.getOrElse { - sys.error("Failed to create change: " + e) - } - } - } - } - } - } - } - } - - def findByGuid(authorization: Authorization, guid: UUID): Option[Change] = { - findAll(authorization, guid = Some(guid), limit = 1).headOption - } - - def findAll( - authorization: Authorization, - guid: Option[UUID] = None, - organizationGuid: Option[UUID] = None, - organizationKey: Option[String] = None, - applicationKey: Option[String] = None, - applicationGuid: Option[UUID] = None, - fromVersionGuid: Option[UUID] = None, - toVersionGuid: Option[UUID] = None, - fromVersion: Option[String] = None, - toVersion: Option[String] = None, - `type`: Option[String] = None, - description: Option[String] = None, - limit: Long = 25, - offset: Long = 0 - ): Seq[Change] = { - db.withConnection { implicit c => - authorization.applicationFilter(BaseQuery, "applications.guid"). - equals("changes.guid", guid). - equals("organizations.guid", organizationGuid). - equals("organizations.key", organizationKey). - equals("applications.guid", applicationGuid). - equals("applications.key", applicationKey). - equals("changes.from_version_guid", fromVersionGuid). - equals("changes.to_version_guid", toVersionGuid). - greaterThanOrEquals("from_version.version_sort_key", fromVersion.map(VersionTag(_).sortKey)). - lessThanOrEquals("to_version.version_sort_key", toVersion.map(VersionTag(_).sortKey)). - equals("changes.type", `type`). - and( - description.map { _ => - "lower(changes.description) = lower(trim({description}))" - } - ).bind("description", description). - orderBy("changes.changed_at desc, lower(organizations.key), lower(applications.key), changes.type, lower(changes.description)"). - limit(limit). - offset(offset). - as(parser.*) - } - } - - - private val parser: RowParser[Change] = { - import org.joda.time.DateTime - - SqlParser.get[UUID]("guid") ~ - SqlParser.get[UUID]("organization_guid") ~ - SqlParser.str("organization_key") ~ - SqlParser.get[UUID]("application_guid") ~ - SqlParser.str("application_key") ~ - SqlParser.get[UUID]("from_version_guid") ~ - SqlParser.str("from_version_version") ~ - SqlParser.get[UUID]("to_version_guid") ~ - SqlParser.str("to_version_version") ~ - SqlParser.str("type") ~ - SqlParser.str("description") ~ - SqlParser.bool("is_material") ~ - SqlParser.get[DateTime]("changed_at") ~ - SqlParser.get[UUID]("changed_by_guid") ~ - SqlParser.str("changed_by_nickname") ~ - SqlParser.get[DateTime]("created_at") ~ - SqlParser.get[UUID]("created_by_guid") ~ - SqlParser.get[DateTime]("updated_at") ~ - SqlParser.get[UUID]("updated_by_guid") map { - case guid ~ organizationGuid ~ organizationKey ~ applicationGuid ~ applicationKey ~ fromVersionGuid ~ fromVersionVersion ~ toVersionGuid ~ toVersionVersion ~ typ ~ description ~ isMaterial ~ changedAt ~ changedByGuid ~ changedByNickname ~ createdAt ~ createdByGuid ~ updatedAt ~ updatedByGuid => { - Change( - guid = guid, - organization = Reference(guid = organizationGuid, key = organizationKey), - application = Reference(guid = applicationGuid, key = applicationKey), - fromVersion = ChangeVersion(guid = fromVersionGuid, version = fromVersionVersion), - toVersion = ChangeVersion(guid = toVersionGuid, version = toVersionVersion), - diff = typ match { - case DiffType.Breaking => DiffBreaking(description = description, isMaterial = isMaterial) - case DiffType.NonBreaking => DiffNonBreaking(description = description, isMaterial = isMaterial) - case other => sys.error(s"Invalid diff type '$other'") - }, - changedAt = changedAt, - changedBy = UserSummary(guid = changedByGuid, nickname = changedByNickname), - audit = Audit( - createdAt = createdAt, - createdBy = ReferenceGuid(createdByGuid), - updatedAt = updatedAt, - updatedBy = ReferenceGuid(updatedByGuid), - ) - ) - } - } - } -} \ No newline at end of file diff --git a/api/app/db/InternalApplicationsDao.scala b/api/app/db/InternalApplicationsDao.scala index 5e3228527..898cff61e 100644 --- a/api/app/db/InternalApplicationsDao.scala +++ b/api/app/db/InternalApplicationsDao.scala @@ -228,6 +228,10 @@ class InternalApplicationsDao @Inject()( findAll(authorization, guid = Some(guid), limit = Some(1)).headOption } + def findAllByGuids(authorization: Authorization, guids: Seq[UUID]): Seq[InternalApplication] = { + findAll(authorization, guids = Some(guids), limit = None) + } + def findAll( authorization: Authorization, orgKey: Option[String] = None, diff --git a/api/app/db/InternalChangesDao.scala b/api/app/db/InternalChangesDao.scala new file mode 100644 index 000000000..b4e5b1147 --- /dev/null +++ b/api/app/db/InternalChangesDao.scala @@ -0,0 +1,175 @@ +package db + +import anorm.JodaParameterMetaData.* +import anorm.* +import db.generated.{ChangeForm, ChangesDao} +import io.apibuilder.api.v0.models.* +import io.flow.postgresql.{OrderBy, Query} +import lib.VersionTag +import org.postgresql.util.PSQLException +import util.OptionalQueryFilter + +import java.util.UUID +import javax.inject.Inject +import scala.util.{Failure, Success, Try} + +case class InternalChange(db: generated.Change) { + val guid: UUID = db.guid + val diff: Diff = db.`type` match { + case "breaking" => DiffBreaking(description = db.description, isMaterial = db.isMaterial) + case "non_breaking" => DiffNonBreaking(description = db.description, isMaterial = db.isMaterial) + case other => sys.error(s"Invalid diff type '$other'") + } +} + +class InternalChangesDao @Inject()( + dao: ChangesDao +) { + private object DiffType { + val Breaking = "breaking" + val NonBreaking = "non_breaking" + } + + def upsert( + createdBy: User, + fromVersion: Version, + toVersion: Version, + differences: Seq[Diff] + ): Unit = { + assert( + fromVersion.guid != toVersion.guid, + "Versions must be different" + ) + + assert( + fromVersion.application.guid == toVersion.application.guid, + "Versions must belong to same application" + ) + + dao.db.withTransaction { implicit c => + differences.map { + case d: DiffBreaking => (DiffType.Breaking, d) + case d: DiffNonBreaking => (DiffType.NonBreaking, d) + case DiffUndefinedType(desc) => sys.error(s"Unrecognized difference type: $desc") + }.distinct.foreach { + case (differenceType, diff) => { + val form = ChangeForm( + applicationGuid = fromVersion.application.guid, + fromVersionGuid = fromVersion.guid, + toVersionGuid = toVersion.guid, + `type` = differenceType, + description = diff.description, + isMaterial = diff.isMaterial, + changedAt = toVersion.audit.createdAt, + changedByGuid = toVersion.audit.createdBy.guid, + ) + Try { + dao.insert(createdBy.guid, form) + } match { + case Success(_) => // no-op + case Failure(e) => e match { + case e: PSQLException if exists(form) => // no-op as already exists + case t: Throwable => throw t + } + } + } + } + } + } + + private def exists(form: ChangeForm): Boolean = { + findAll( + Authorization.All, + fromVersionGuid = Some(form.fromVersionGuid), + toVersionGuid = Some(form.toVersionGuid), + description = Some(form.description), + limit = Some(1), + ).nonEmpty + } + + def findByGuid(authorization: Authorization, guid: UUID): Option[InternalChange] = { + findAll(authorization, guid = Some(guid), limit = Some(1)).headOption + } + + def findAll( + authorization: Authorization, + guid: Option[UUID] = None, + organizationGuid: Option[UUID] = None, + organizationKey: Option[String] = None, + applicationKey: Option[String] = None, + applicationGuid: Option[UUID] = None, + fromVersionGuid: Option[UUID] = None, + toVersionGuid: Option[UUID] = None, + fromVersion: Option[String] = None, + toVersion: Option[String] = None, + `type`: Option[String] = None, + description: Option[String] = None, + isDeleted: Option[Boolean] = None, + limit: Option[Long], + offset: Long = 0 + ): Seq[InternalChange] = { + val filters = List( + new OptionalQueryFilter(organizationGuid) { + override def filter(q: Query, orgGuid: UUID): Query = { + q.in("application_guid", Query("select guid from applications").equals("organization_guid", orgGuid)) + } + }, + new OptionalQueryFilter(organizationKey) { + override def filter(q: Query, key: String): Query = { + q.in("application_guid", Query( + """ + |select app.guid + | from applications app + | join organizations org on org.guid = app.organization_guid + |""".stripMargin + ).equals("org.key", key)) + } + }, + new OptionalQueryFilter(applicationKey) { + override def filter(q: Query, key: String): Query = { + q.in("application_guid", Query("select guid from applications").equals("key", key)) + } + }, + new OptionalQueryFilter(fromVersion) { + override def filter(q: Query, v: String): Query = { + q.in( + "from_version_guid", + Query("select guid from versions") + .greaterThanOrEquals("version_sort_key", VersionTag(v).sortKey) + ) + } + }, + new OptionalQueryFilter(toVersion) { + override def filter(q: Query, v: String): Query = { + q.in( + "from_version_guid", + Query("select guid from versions") + .lessThanOrEquals("version_sort_key", VersionTag(v).sortKey) + ) + } + }, + ) + + dao.findAll( + guid = guid, + applicationGuid = applicationGuid, + toVersionGuid = toVersionGuid, + limit = limit, + offset = offset, + orderBy = Some(OrderBy("-changed_at, type, -description")), + ) { q => + authorization.applicationFilter( + filters.foldLeft(q) { case (q, f) => f.filter(q) }, + "application_guid" + ) + .equals("from_version_guid", fromVersionGuid) + .equals("type", `type`) + .and(isDeleted.map(Filters.isDeleted("changes", _))) + .and( + description.map { _ => + "lower(description) = lower(trim({description}))" + } + ).bind("description", description) + }.map(InternalChange(_)) + } +} \ No newline at end of file diff --git a/api/app/db/InternalOrganizationsDao.scala b/api/app/db/InternalOrganizationsDao.scala index 5b9e65926..060c9370a 100644 --- a/api/app/db/InternalOrganizationsDao.scala +++ b/api/app/db/InternalOrganizationsDao.scala @@ -181,6 +181,10 @@ class InternalOrganizationsDao @Inject()( findAll(authorization, guid = Some(guid), limit = Some(1)).headOption } + def findAllByGuids(authorization: Authorization, guids: Seq[UUID]): Seq[InternalOrganization] = { + findAll(authorization, guids = Some(guids), limit = None) + } + def findByKey(authorization: Authorization, orgKey: String): Option[InternalOrganization] = { findAll(authorization, key = Some(orgKey), limit = Some(1)).headOption } diff --git a/api/app/db/UsersDao.scala b/api/app/db/UsersDao.scala index 2bc1c8508..164a4848d 100644 --- a/api/app/db/UsersDao.scala +++ b/api/app/db/UsersDao.scala @@ -237,14 +237,19 @@ class UsersDao @Inject() ( findAll(guid = Some(guid)).headOption } + def findAllByGuids(guids: Seq[UUID]): Seq[User] = { + findAll(guids = Some(guids), limit = None) + } + def findAll( - guid: Option[UUID] = None, - guids: Option[Seq[UUID]] = None, + guid: Option[UUID] = None, + guids: Option[Seq[UUID]] = None, email: Option[String] = None, nickname: Option[String] = None, sessionId: Option[String] = None, token: Option[String] = None, - isDeleted: Option[Boolean] = Some(false) + isDeleted: Option[Boolean] = Some(false), + limit: Option[Long] = None, ): Seq[User] = { require( guid.isDefined || guids.isDefined || email.isDefined || token.isDefined || sessionId.isDefined || nickname.isDefined, @@ -268,7 +273,7 @@ class UsersDao @Inject() ( token.map { _ => "guid = (select user_guid from tokens where token = {token} and deleted_at is null)" } ).bind("token", token). and(isDeleted.map(Filters.isDeleted("users", _))). - limit(1). + optionalLimit(limit). as(parser.*) } } diff --git a/api/app/db/VersionsDao.scala b/api/app/db/VersionsDao.scala index 7c51ad461..21e7961fa 100644 --- a/api/app/db/VersionsDao.scala +++ b/api/app/db/VersionsDao.scala @@ -108,7 +108,7 @@ class VersionsDao @Inject() ( val latestVersion: Option[InternalVersion] = findAll( Authorization.User(user.guid), applicationGuid = Some(application.guid), - limit = 1 + limit = Some(1) ).headOption val guid = db.withTransaction { implicit c => @@ -197,13 +197,13 @@ class VersionsDao @Inject() ( ): Option[InternalVersion] = { applicationsDao.findByOrganizationKeyAndApplicationKey(authorization, orgKey, applicationKey).flatMap { application => if (version == LatestVersion) { - findAll(authorization, applicationGuid = Some(application.guid), limit = 1).headOption + findAll(authorization, applicationGuid = Some(application.guid), limit = Some(1)).headOption } else if (version.startsWith(LatestVersionFilter)) { /* ~ specifies a minimum version, but allows the last digit specified to go up */ val versionFilter = version.replace(LatestVersionFilter, "") - findAll(authorization, applicationGuid = Some(application.guid), limit = 1 + findAll(authorization, applicationGuid = Some(application.guid), limit = Some(1) , versionConstraint = Some(versionFilter.split("\\.").dropRight(1).mkString(".")) //allows the last digit specified to go up ) .headOption @@ -219,7 +219,7 @@ class VersionsDao @Inject() ( authorization, applicationGuid = Some(application.guid), version = Some(version), - limit = 1 + limit = Some(1) ).headOption } @@ -228,29 +228,35 @@ class VersionsDao @Inject() ( guid: UUID, isDeleted: Option[Boolean] = Some(false) ): Option[InternalVersion] = { - findAll(authorization, guid = Some(guid), isDeleted = isDeleted, limit = 1).headOption + findAll(authorization, guid = Some(guid), isDeleted = isDeleted, limit = Some(1)).headOption + } + + def findAllByGuids(authorization: Authorization, guids: Seq[UUID]): Seq[InternalVersion] = { + findAll(authorization, guids = Some(guids), limit = None) } def findAll( authorization: Authorization, applicationGuid: Option[UUID] = None, guid: Option[UUID] = None, + guids: Option[Seq[UUID]] = None, version: Option[String] = None, versionConstraint: Option[String] = None, isDeleted: Option[Boolean] = Some(false), - limit: Long = 25, + limit: Option[Long], offset: Long = 0 ): Seq[InternalVersion] = { db.withConnection { implicit c => authorization.applicationFilter(BaseQuery, "application_guid"). and(HasServiceJsonClause). equals("versions.guid", guid). + optionalIn("versions.guid", guids). equals("versions.application_guid", applicationGuid). equals("versions.version", version). and(versionConstraint.map(vc => s"versions.version like '${vc}%'")). and(isDeleted.map(Filters.isDeleted("versions", _))). orderBy("versions.version_sort_key desc, versions.created_at desc"). - limit(limit). + optionalLimit(limit). offset(offset). as(parser.*) } diff --git a/api/app/models/ChangesModel.scala b/api/app/models/ChangesModel.scala new file mode 100644 index 000000000..044bd9e9d --- /dev/null +++ b/api/app/models/ChangesModel.scala @@ -0,0 +1,69 @@ +package models + +import db.* +import io.apibuilder.api.v0.models.{Change, ChangeVersion, Organization, UserSummary} +import io.apibuilder.common.v0.models.{Audit, Reference, ReferenceGuid} + +import java.util.UUID +import javax.inject.Inject + +class ChangesModel @Inject()( + orgDao: InternalOrganizationsDao, + appDao: InternalApplicationsDao, + versionsDao: VersionsDao, + usersDao: UsersDao, +) { + def toModel(v: InternalChange): Option[Change] = { + toModels(Seq(v)).headOption + } + + def toModels(changes: Seq[InternalChange]): Seq[Change] = { + val apps = appDao.findAllByGuids( + Authorization.All, + changes.map(_.db.applicationGuid).distinct + ) + val appsByGuid = apps.map { a => a.guid -> a }.toMap + val orgsByGuid = orgDao.findAllByGuids( + Authorization.All, + apps.map(_.db.organizationGuid).distinct + ).map { o => o.guid -> o }.toMap + + val versionsByGuid = versionsDao.findAllByGuids( + Authorization.All, + changes.flatMap { c => + Seq(c.db.fromVersionGuid, c.db.toVersionGuid) + }.distinct + ).map { v => v.guid -> v }.toMap + + val usersByGuid = usersDao.findAllByGuids( + changes.map(_.db.changedByGuid).distinct + ).map { u => u.guid -> u }.toMap + + changes.flatMap { change => + for { + app <- appsByGuid.get(change.db.applicationGuid) + org <- orgsByGuid.get(app.db.organizationGuid) + fromVersion <- versionsByGuid.get(change.db.fromVersionGuid) + toVersion <- versionsByGuid.get(change.db.toVersionGuid) + changedBy <- usersByGuid.get(change.db.toVersionGuid) + } yield { + Change( + guid = change.guid, + organization = Reference(guid = org.guid, key = org.key), + application = Reference(guid = app.guid, key = app.key), + fromVersion = ChangeVersion(guid = fromVersion.guid, version = fromVersion.version), + toVersion = ChangeVersion(guid = toVersion.guid, version = toVersion.version), + diff = change.diff, + changedAt = change.db.changedAt, + changedBy = UserSummary(guid = changedBy.guid, nickname = changedBy.nickname), + audit = Audit( + createdAt = change.db.createdAt, + createdBy = ReferenceGuid(change.db.createdByGuid), + updatedAt = change.db.updatedAt, + updatedBy = ReferenceGuid(change.db.createdByGuid), + ) + ) + } + } + } +} \ No newline at end of file diff --git a/api/app/processor/DiffVersionProcessor.scala b/api/app/processor/DiffVersionProcessor.scala index c6e11659d..e1b0e4c8a 100644 --- a/api/app/processor/DiffVersionProcessor.scala +++ b/api/app/processor/DiffVersionProcessor.scala @@ -20,7 +20,7 @@ class DiffVersionProcessor @Inject()( applicationsDao: InternalApplicationsDao, organizationsDao: InternalOrganizationsDao, emails: Emails, - changesDao: ChangesDao, + changesDao: InternalChangesDao, versionsDao: VersionsDao, versionsModel: VersionsModel, watchesDao: WatchesDao, diff --git a/api/test/db/ChangesDaoSpec.scala b/api/test/db/InternalChangesDaoSpec.scala similarity index 72% rename from api/test/db/ChangesDaoSpec.scala rename to api/test/db/InternalChangesDaoSpec.scala index b8bcdab20..e4785c284 100644 --- a/api/test/db/ChangesDaoSpec.scala +++ b/api/test/db/InternalChangesDaoSpec.scala @@ -7,7 +7,7 @@ import org.scalatestplus.play.guice.GuiceOneAppPerSuite import java.util.UUID -class ChangesDaoSpec extends PlaySpec with GuiceOneAppPerSuite with db.Helpers { +class InternalChangesDaoSpec extends PlaySpec with GuiceOneAppPerSuite with db.Helpers { private def getApplication(version: Version): InternalApplication = { applicationsDao.findByGuid(Authorization.All, version.application.guid).getOrElse { @@ -18,7 +18,7 @@ class ChangesDaoSpec extends PlaySpec with GuiceOneAppPerSuite with db.Helpers { private def createChange( description: String = "Breaking difference - " + UUID.randomUUID.toString, org: InternalOrganization = createOrganization() - ): Change = { + ): InternalChange = { val app = createApplication(org = org) val fromVersion = createVersion(application = app, version = "1.0.0") val toVersion = createVersion(application = app, version = "1.0.1") @@ -27,7 +27,8 @@ class ChangesDaoSpec extends PlaySpec with GuiceOneAppPerSuite with db.Helpers { changesDao.findAll( Authorization.All, - fromVersionGuid = Some(fromVersion.guid) + fromVersionGuid = Some(fromVersion.guid), + limit = Some(1) ).head } @@ -40,7 +41,8 @@ class ChangesDaoSpec extends PlaySpec with GuiceOneAppPerSuite with db.Helpers { changesDao.upsert(testUser, fromVersion, toVersion, Nil) changesDao.findAll( Authorization.All, - fromVersionGuid = Some(fromVersion.guid) + fromVersionGuid = Some(fromVersion.guid), + limit = Some(1) ) must be(Nil) } @@ -55,7 +57,8 @@ class ChangesDaoSpec extends PlaySpec with GuiceOneAppPerSuite with db.Helpers { val actual = changesDao.findAll( Authorization.All, - fromVersionGuid = Some(fromVersion.guid) + fromVersionGuid = Some(fromVersion.guid), + limit = None ).map(_.diff) actual.size must be(2) @@ -73,7 +76,8 @@ class ChangesDaoSpec extends PlaySpec with GuiceOneAppPerSuite with db.Helpers { changesDao.findAll( Authorization.All, - fromVersionGuid = Some(fromVersion.guid) + fromVersionGuid = Some(fromVersion.guid), + limit = None ).map(_.diff) must be(Seq(diff)) } @@ -92,80 +96,83 @@ class ChangesDaoSpec extends PlaySpec with GuiceOneAppPerSuite with db.Helpers { "guid" in { val change = createChange() - changesDao.findAll(Authorization.All, guid = Some(UUID.randomUUID)) must be(Nil) - changesDao.findAll(Authorization.All, guid = Some(change.guid)).map(_.guid) must be(Seq(change.guid)) + changesDao.findAll(Authorization.All, guid = Some(UUID.randomUUID), limit = None) must be(Nil) + changesDao.findAll(Authorization.All, guid = Some(change.guid), limit = None).map(_.guid) must be(Seq(change.guid)) } "organizationGuid" in { val org = createOrganization() val change = createChange(org = org) - changesDao.findAll(Authorization.All, organizationGuid = Some(UUID.randomUUID)) must be(Nil) - changesDao.findAll(Authorization.All, organizationGuid = Some(org.guid)).map(_.guid) must be(Seq(change.guid)) + changesDao.findAll(Authorization.All, organizationGuid = Some(UUID.randomUUID), limit = None) must be(Nil) + changesDao.findAll(Authorization.All, organizationGuid = Some(org.guid), limit = None).map(_.guid) must be(Seq(change.guid)) } "organizationKey" in { val org = createOrganization() val change = createChange(org = org) - changesDao.findAll(Authorization.All, organizationKey = Some(UUID.randomUUID.toString)) must be(Nil) - changesDao.findAll(Authorization.All, organizationKey = Some(org.key)).map(_.guid) must be(Seq(change.guid)) + changesDao.findAll(Authorization.All, organizationKey = Some(UUID.randomUUID.toString), limit = None) must be(Nil) + changesDao.findAll(Authorization.All, organizationKey = Some(org.key), limit = None).map(_.guid) must be(Seq(change.guid)) } "applicationGuid" in { val change = createChange() - changesDao.findAll(Authorization.All, applicationGuid = Some(UUID.randomUUID)) must be(Nil) - changesDao.findAll(Authorization.All, applicationGuid = Some(change.application.guid)).map(_.guid) must be(Seq(change.guid)) + changesDao.findAll(Authorization.All, applicationGuid = Some(UUID.randomUUID), limit = None) must be(Nil) + changesDao.findAll(Authorization.All, applicationGuid = Some(change.db.applicationGuid), limit = None).map(_.guid) must be(Seq(change.guid)) } "applicationKey" in { val change = createChange() - changesDao.findAll(Authorization.All, applicationKey = Some(UUID.randomUUID.toString)) must be(Nil) - changesDao.findAll(Authorization.All, applicationKey = Some(change.application.key)).map(_.guid) must be(Seq(change.guid)) + val app = applicationsDao.findByGuid(Authorization.All, change.db.applicationGuid).get + changesDao.findAll(Authorization.All, applicationKey = Some(UUID.randomUUID.toString), limit = None) must be(Nil) + changesDao.findAll(Authorization.All, applicationKey = Some(app.key), limit = None).map(_.guid) must be(Seq(change.guid)) } "fromVersionGuid" in { val change = createChange() - changesDao.findAll(Authorization.All, fromVersionGuid = Some(UUID.randomUUID)) must be(Nil) - changesDao.findAll(Authorization.All, fromVersionGuid = Some(change.fromVersion.guid)).map(_.guid) must be(Seq(change.guid)) + changesDao.findAll(Authorization.All, fromVersionGuid = Some(UUID.randomUUID), limit = None) must be(Nil) + changesDao.findAll(Authorization.All, fromVersionGuid = Some(change.db.fromVersionGuid), limit = None).map(_.guid) must be(Seq(change.guid)) } "toVersionGuid" in { val change = createChange() - changesDao.findAll(Authorization.All, toVersionGuid = Some(UUID.randomUUID)) must be(Nil) - changesDao.findAll(Authorization.All, toVersionGuid = Some(change.toVersion.guid)).map(_.guid) must be(Seq(change.guid)) + changesDao.findAll(Authorization.All, toVersionGuid = Some(UUID.randomUUID), limit = None) must be(Nil) + changesDao.findAll(Authorization.All, toVersionGuid = Some(change.db.toVersionGuid), limit = None).map(_.guid) must be(Seq(change.guid)) } "fromVersion" in { val change = createChange() - changesDao.findAll(Authorization.All, guid = Some(change.guid), fromVersion = Some(change.fromVersion.version)).map(_.guid) must be(Seq(change.guid)) + val version = versionsDao.findByGuid(Authorization.All, change.db.fromVersionGuid).get + changesDao.findAll(Authorization.All, guid = Some(change.guid), fromVersion = Some(version.version), limit = None).map(_.guid) must be(Seq(change.guid)) } "toVersion" in { val change = createChange() - changesDao.findAll(Authorization.All, toVersion = Some(UUID.randomUUID.toString)) must be(Nil) - changesDao.findAll(Authorization.All, guid = Some(change.guid), toVersion = Some(change.toVersion.version)).map(_.guid) must be(Seq(change.guid)) + val version = versionsDao.findByGuid(Authorization.All, change.db.toVersionGuid).get + changesDao.findAll(Authorization.All, toVersion = Some(UUID.randomUUID.toString), limit = None) must be(Nil) + changesDao.findAll(Authorization.All, guid = Some(change.guid), toVersion = Some(version.version), limit = None).map(_.guid) must be(Seq(change.guid)) } "description" in { val desc = UUID.randomUUID.toString val change = createChange(description = desc) - changesDao.findAll(Authorization.All, description = Some(UUID.randomUUID.toString)) must be(Nil) - changesDao.findAll(Authorization.All, description = Some(desc)).map(_.guid) must be(Seq(change.guid)) + changesDao.findAll(Authorization.All, description = Some(UUID.randomUUID.toString), limit = None) must be(Nil) + changesDao.findAll(Authorization.All, description = Some(desc), limit = None).map(_.guid) must be(Seq(change.guid)) } "limit and offset" in { val change = createChange() - changesDao.findAll(Authorization.All, guid = Some(change.guid), limit = 1).map(_.guid) must be(Seq(change.guid)) - changesDao.findAll(Authorization.All, guid = Some(change.guid), limit = 1, offset = 1).map(_.guid) must be(Nil) + changesDao.findAll(Authorization.All, guid = Some(change.guid), limit = Some(1)).map(_.guid) must be(Seq(change.guid)) + changesDao.findAll(Authorization.All, guid = Some(change.guid), limit = Some(1), offset = 1).map(_.guid) must be(Nil) } "authorization" in { val change = createChange() val user = createRandomUser() - changesDao.findAll(Authorization.User(user.guid), guid = Some(change.guid)).map(_.guid) must be(Nil) - changesDao.findAll(Authorization.PublicOnly, guid = Some(change.guid)).map(_.guid) must be(Nil) + changesDao.findAll(Authorization.User(user.guid), guid = Some(change.guid), limit = None).map(_.guid) must be(Nil) + changesDao.findAll(Authorization.PublicOnly, guid = Some(change.guid), limit = None).map(_.guid) must be(Nil) - val app = applicationsDao.findByGuid(Authorization.All, change.application.guid).get + val app = applicationsDao.findByGuid(Authorization.All, change.db.applicationGuid).get createMembership( organizationsDao.findByGuid( @@ -174,7 +181,7 @@ class ChangesDaoSpec extends PlaySpec with GuiceOneAppPerSuite with db.Helpers { ).get, user ) - changesDao.findAll(Authorization.User(testUser.guid), guid = Some(change.guid)).map(_.guid) must be(Seq(change.guid)) + changesDao.findAll(Authorization.User(testUser.guid), guid = Some(change.guid), limit = None).map(_.guid) must be(Seq(change.guid)) } } diff --git a/api/test/db/VersionsDaoSpec.scala b/api/test/db/VersionsDaoSpec.scala index bc0aa7405..0bab23007 100644 --- a/api/test/db/VersionsDaoSpec.scala +++ b/api/test/db/VersionsDaoSpec.scala @@ -56,7 +56,8 @@ class VersionsDaoSpec extends PlaySpec with GuiceOneAppPerSuite with db.Helpers versionsDao.findAll( Authorization.All, - applicationGuid = Some(app.guid) + applicationGuid = Some(app.guid), + limit = None ).map(_.version) must be(Seq("1.0.2", "1.0.2-dev")) } diff --git a/api/test/util/Daos.scala b/api/test/util/Daos.scala index a6b82fd6b..2d4d36271 100644 --- a/api/test/util/Daos.scala +++ b/api/test/util/Daos.scala @@ -14,7 +14,7 @@ trait Daos { def applicationsDao: InternalApplicationsDao = injector.instanceOf[db.InternalApplicationsDao] def attributesDao: InternalAttributesDao = injector.instanceOf[db.InternalAttributesDao] - def changesDao: ChangesDao = injector.instanceOf[db.ChangesDao] + def changesDao: InternalChangesDao = injector.instanceOf[db.InternalChangesDao] def databaseServiceFetcher: DatabaseServiceFetcher = injector.instanceOf[DatabaseServiceFetcher] def emailVerificationsDao: EmailVerificationsDao = injector.instanceOf[db.EmailVerificationsDao] def itemsDao: ItemsDao = injector.instanceOf[db.ItemsDao] diff --git a/dao/spec/psql-apibuilder.json b/dao/spec/psql-apibuilder.json index 7482f61dd..a7ff0fee6 100644 --- a/dao/spec/psql-apibuilder.json +++ b/dao/spec/psql-apibuilder.json @@ -163,6 +163,53 @@ ] }, + "change": { + "fields": [ + { "name": "guid", "type": "uuid" }, + { "name": "application_guid", "type": "uuid" }, + { "name": "from_version_guid", "type": "uuid" }, + { "name": "to_version_guid", "type": "uuid" }, + { "name": "type", "type": "string" }, + { "name": "description", "type": "string" }, + { "name": "changed_at", "type": "date-time-iso8601" }, + { "name": "changed_by_guid", "type": "uuid" }, + { "name": "is_material", "type": "boolean" } + ], + "attributes": [ + { + "name": "scala", + "value": { + "pkey_generator": { + "method": "java.util.UUID.randomUUID", + "returns": "uuid" + } + } + }, + { + "name": "psql", + "value": { + "audit": { + "created": { + "at": { "type": "date-time-iso8601" }, + "by": { "name": "created_by_guid", "type": "uuid" } + }, + "updated": { + "at": { "type": "date-time-iso8601" } + }, + "deleted": { + "at": { "type": "date-time-iso8601", "required": false }, + "by": { "name": "deleted_by_guid", "type": "uuid", "required": false } + } + }, + "indexes": [ + { "fields": ["application_guid"] }, + { "fields": ["to_version_guid"] } + ] + } + } + ] + }, + "task": { "fields": [ { "name": "id", "type": "string" }, diff --git a/generated/app/db/ChangesDao.scala b/generated/app/db/ChangesDao.scala new file mode 100644 index 000000000..177a61efa --- /dev/null +++ b/generated/app/db/ChangesDao.scala @@ -0,0 +1,637 @@ +package db.generated + +case class Change( + guid: java.util.UUID, + applicationGuid: java.util.UUID, + fromVersionGuid: java.util.UUID, + toVersionGuid: java.util.UUID, + `type`: String, + description: String, + changedAt: org.joda.time.DateTime, + changedByGuid: java.util.UUID, + isMaterial: Boolean, + createdAt: org.joda.time.DateTime, + createdByGuid: java.util.UUID, + updatedAt: org.joda.time.DateTime, + deletedAt: Option[org.joda.time.DateTime], + deletedByGuid: Option[java.util.UUID] +) { + def form: ChangeForm = { + ChangeForm( + applicationGuid = applicationGuid, + fromVersionGuid = fromVersionGuid, + toVersionGuid = toVersionGuid, + `type` = `type`, + description = description, + changedAt = changedAt, + changedByGuid = changedByGuid, + isMaterial = isMaterial, + ) + } +} + +case class ChangeForm( + applicationGuid: java.util.UUID, + fromVersionGuid: java.util.UUID, + toVersionGuid: java.util.UUID, + `type`: String, + description: String, + changedAt: org.joda.time.DateTime, + changedByGuid: java.util.UUID, + isMaterial: Boolean +) + +case object ChangesTable { + val SchemaName: String = "public" + + val TableName: String = "changes" + + val QualifiedName: String = "public.changes" + + sealed trait Column { + def name: String + } + + object Columns { + case object Guid extends Column { + override val name: String = "guid" + } + + case object ApplicationGuid extends Column { + override val name: String = "application_guid" + } + + case object FromVersionGuid extends Column { + override val name: String = "from_version_guid" + } + + case object ToVersionGuid extends Column { + override val name: String = "to_version_guid" + } + + case object Type extends Column { + override val name: String = "type" + } + + case object Description extends Column { + override val name: String = "description" + } + + case object ChangedAt extends Column { + override val name: String = "changed_at" + } + + case object ChangedByGuid extends Column { + override val name: String = "changed_by_guid" + } + + case object IsMaterial extends Column { + override val name: String = "is_material" + } + + case object CreatedAt extends Column { + override val name: String = "created_at" + } + + case object CreatedByGuid extends Column { + override val name: String = "created_by_guid" + } + + case object UpdatedAt extends Column { + override val name: String = "updated_at" + } + + case object DeletedAt extends Column { + override val name: String = "deleted_at" + } + + case object DeletedByGuid extends Column { + override val name: String = "deleted_by_guid" + } + + val all: List[Column] = List(Guid, ApplicationGuid, FromVersionGuid, ToVersionGuid, Type, Description, ChangedAt, ChangedByGuid, IsMaterial, CreatedAt, CreatedByGuid, UpdatedAt, DeletedAt, DeletedByGuid) + } +} + +trait BaseChangesDao { + import anorm.* + + import anorm.JodaParameterMetaData.* + + import anorm.postgresql.* + + def db: play.api.db.Database + + private val BaseQuery: io.flow.postgresql.Query = { + io.flow.postgresql.Query(""" + | select guid::text, + | application_guid::text, + | from_version_guid::text, + | to_version_guid::text, + | type, + | description, + | changed_at, + | changed_by_guid::text, + | is_material, + | created_at, + | created_by_guid::text, + | updated_at, + | deleted_at, + | deleted_by_guid::text + | from public.changes + |""".stripMargin.stripTrailing + ) + } + + def findAll( + guid: Option[java.util.UUID] = None, + guids: Option[Seq[java.util.UUID]] = None, + applicationGuid: Option[java.util.UUID] = None, + applicationGuids: Option[Seq[java.util.UUID]] = None, + toVersionGuid: Option[java.util.UUID] = None, + toVersionGuids: Option[Seq[java.util.UUID]] = None, + limit: Option[Long], + offset: Long = 0, + orderBy: Option[io.flow.postgresql.OrderBy] = None + )(implicit customQueryModifier: io.flow.postgresql.Query => io.flow.postgresql.Query = identity): Seq[Change] = { + db.withConnection { c => + findAllWithConnection(c, guid, guids, applicationGuid, applicationGuids, toVersionGuid, toVersionGuids, limit, offset, orderBy) + } + } + + def findAllWithConnection( + c: java.sql.Connection, + guid: Option[java.util.UUID] = None, + guids: Option[Seq[java.util.UUID]] = None, + applicationGuid: Option[java.util.UUID] = None, + applicationGuids: Option[Seq[java.util.UUID]] = None, + toVersionGuid: Option[java.util.UUID] = None, + toVersionGuids: Option[Seq[java.util.UUID]] = None, + limit: Option[Long], + offset: Long = 0, + orderBy: Option[io.flow.postgresql.OrderBy] = None + )(implicit customQueryModifier: io.flow.postgresql.Query => io.flow.postgresql.Query = identity): Seq[Change] = { + customQueryModifier(BaseQuery) + .equals("changes.guid", guid) + .optionalIn("changes.guid", guids) + .equals("changes.application_guid", applicationGuid) + .optionalIn("changes.application_guid", applicationGuids) + .equals("changes.to_version_guid", toVersionGuid) + .optionalIn("changes.to_version_guid", toVersionGuids) + .optionalLimit(limit) + .offset(offset) + .orderBy(orderBy.flatMap(_.sql)) + .as(parser.*)(c) + } + + def iterateAll( + guid: Option[java.util.UUID] = None, + guids: Option[Seq[java.util.UUID]] = None, + applicationGuid: Option[java.util.UUID] = None, + applicationGuids: Option[Seq[java.util.UUID]] = None, + toVersionGuid: Option[java.util.UUID] = None, + toVersionGuids: Option[Seq[java.util.UUID]] = None, + pageSize: Long = 1000 + )(implicit customQueryModifier: io.flow.postgresql.Query => io.flow.postgresql.Query = identity): Iterator[Change] = { + assert(pageSize > 0, "pageSize must be > 0") + + def iterate(lastValue: Option[Change]): Iterator[Change] = { + val page: Seq[Change] = db.withConnection { c => + customQueryModifier(BaseQuery) + .equals("changes.guid", guid) + .optionalIn("changes.guid", guids) + .equals("changes.application_guid", applicationGuid) + .optionalIn("changes.application_guid", applicationGuids) + .equals("changes.to_version_guid", toVersionGuid) + .optionalIn("changes.to_version_guid", toVersionGuids) + .greaterThan("changes.guid", lastValue.map(_.guid)) + .orderBy("changes.guid") + .limit(pageSize) + .as(parser.*)(c) + } + if (page.length >= pageSize) { + page.iterator ++ iterate(page.lastOption) + } else { + page.iterator + } + } + + iterate(None) + } + + def findByGuid(guid: java.util.UUID): Option[Change] = { + db.withConnection { c => + findByGuidWithConnection(c, guid) + } + } + + def findByGuidWithConnection( + c: java.sql.Connection, + guid: java.util.UUID + ): Option[Change] = { + findAllWithConnection( + c = c, + guid = Some(guid), + limit = Some(1) + ).headOption + } + + def findAllByApplicationGuid(applicationGuid: java.util.UUID): Seq[Change] = { + db.withConnection { c => + findAllByApplicationGuidWithConnection(c, applicationGuid) + } + } + + def findAllByApplicationGuidWithConnection( + c: java.sql.Connection, + applicationGuid: java.util.UUID + ): Seq[Change] = { + findAllWithConnection( + c = c, + applicationGuid = Some(applicationGuid), + limit = None + ) + } + + def findAllByToVersionGuid(toVersionGuid: java.util.UUID): Seq[Change] = { + db.withConnection { c => + findAllByToVersionGuidWithConnection(c, toVersionGuid) + } + } + + def findAllByToVersionGuidWithConnection( + c: java.sql.Connection, + toVersionGuid: java.util.UUID + ): Seq[Change] = { + findAllWithConnection( + c = c, + toVersionGuid = Some(toVersionGuid), + limit = None + ) + } + + private val parser: anorm.RowParser[Change] = { + anorm.SqlParser.str("guid") ~ + anorm.SqlParser.str("application_guid") ~ + anorm.SqlParser.str("from_version_guid") ~ + anorm.SqlParser.str("to_version_guid") ~ + anorm.SqlParser.str("type") ~ + anorm.SqlParser.str("description") ~ + anorm.SqlParser.get[org.joda.time.DateTime]("changed_at") ~ + anorm.SqlParser.str("changed_by_guid") ~ + anorm.SqlParser.bool("is_material") ~ + anorm.SqlParser.get[org.joda.time.DateTime]("created_at") ~ + anorm.SqlParser.str("created_by_guid") ~ + anorm.SqlParser.get[org.joda.time.DateTime]("updated_at") ~ + anorm.SqlParser.get[org.joda.time.DateTime]("deleted_at").? ~ + anorm.SqlParser.str("deleted_by_guid").? map { case guid ~ applicationGuid ~ fromVersionGuid ~ toVersionGuid ~ type_ ~ description ~ changedAt ~ changedByGuid ~ isMaterial ~ createdAt ~ createdByGuid ~ updatedAt ~ deletedAt ~ deletedByGuid => + Change( + guid = java.util.UUID.fromString(guid), + applicationGuid = java.util.UUID.fromString(applicationGuid), + fromVersionGuid = java.util.UUID.fromString(fromVersionGuid), + toVersionGuid = java.util.UUID.fromString(toVersionGuid), + `type` = type_, + description = description, + changedAt = changedAt, + changedByGuid = java.util.UUID.fromString(changedByGuid), + isMaterial = isMaterial, + createdAt = createdAt, + createdByGuid = java.util.UUID.fromString(createdByGuid), + updatedAt = updatedAt, + deletedAt = deletedAt, + deletedByGuid = deletedByGuid.map { v => java.util.UUID.fromString(v) } + ) + } + } +} + +class ChangesDao @javax.inject.Inject() (override val db: play.api.db.Database) extends BaseChangesDao { + import anorm.JodaParameterMetaData.* + + import anorm.postgresql.* + + def randomPkey: java.util.UUID = { + java.util.UUID.randomUUID + } + + private val InsertQuery: io.flow.postgresql.Query = { + io.flow.postgresql.Query(""" + | insert into public.changes + | (guid, application_guid, from_version_guid, to_version_guid, type, description, changed_at, changed_by_guid, is_material, created_at, created_by_guid, updated_at) + | values + | ({guid}::uuid, {application_guid}::uuid, {from_version_guid}::uuid, {to_version_guid}::uuid, {type}, {description}, {changed_at}::timestamptz, {changed_by_guid}::uuid, {is_material}::boolean, {created_at}::timestamptz, {created_by_guid}::uuid, {updated_at}::timestamptz) + """.stripMargin) + } + + private val UpdateQuery: io.flow.postgresql.Query = { + io.flow.postgresql.Query(""" + | update public.changes + | set application_guid = {application_guid}::uuid, + | from_version_guid = {from_version_guid}::uuid, + | to_version_guid = {to_version_guid}::uuid, + | type = {type}, + | description = {description}, + | changed_at = {changed_at}::timestamptz, + | changed_by_guid = {changed_by_guid}::uuid, + | is_material = {is_material}::boolean, + | updated_at = {updated_at}::timestamptz + | where guid = {guid}::uuid + """.stripMargin) + } + + private val DeleteQuery: io.flow.postgresql.Query = { + io.flow.postgresql.Query("update public.changes set deleted_at = {deleted_at}::timestamptz, deleted_by_guid = {deleted_by_guid}::uuid") + } + + def insert( + user: java.util.UUID, + form: ChangeForm + ): java.util.UUID = { + db.withConnection { c => + insert(c, user, form) + } + } + + def insert( + c: java.sql.Connection, + user: java.util.UUID, + form: ChangeForm + ): java.util.UUID = { + val id = randomPkey + bindQuery(InsertQuery, user, form) + .bind("created_at", org.joda.time.DateTime.now) + .bind("created_by_guid", user) + .bind("guid", id) + .execute(c) + id + } + + def insertBatch( + user: java.util.UUID, + forms: Seq[ChangeForm] + ): Seq[java.util.UUID] = { + db.withConnection { c => + insertBatch(c, user, forms) + } + } + + def insertBatch( + c: java.sql.Connection, + user: java.util.UUID, + forms: Seq[ChangeForm] + ): Seq[java.util.UUID] = { + forms.map { f => + val guid = randomPkey + (guid, Seq(anorm.NamedParameter("created_at", org.joda.time.DateTime.now)) ++ toNamedParameter(user, guid, f)) + }.toList match { + case Nil => Nil + case one :: rest => { + anorm.BatchSql(InsertQuery.sql(), one._2, rest.map(_._2)*).execute()(c) + Seq(one._1) ++ rest.map(_._1) + } + } + } + + def update( + user: java.util.UUID, + change: Change, + form: ChangeForm + ): Unit = { + db.withConnection { c => + update(c, user, change, form) + } + } + + def update( + c: java.sql.Connection, + user: java.util.UUID, + change: Change, + form: ChangeForm + ): Unit = { + updateByGuid( + c = c, + user = user, + guid = change.guid, + form = form + ) + } + + def updateByGuid( + user: java.util.UUID, + guid: java.util.UUID, + form: ChangeForm + ): Unit = { + db.withConnection { c => + updateByGuid(c, user, guid, form) + } + } + + def updateByGuid( + c: java.sql.Connection, + user: java.util.UUID, + guid: java.util.UUID, + form: ChangeForm + ): Unit = { + bindQuery(UpdateQuery, user, form) + .bind("guid", guid) + .execute(c) + () + } + + def updateBatch( + user: java.util.UUID, + forms: Seq[(java.util.UUID, ChangeForm)] + ): Unit = { + db.withConnection { c => + updateBatch(c, user, forms) + } + } + + def updateBatch( + c: java.sql.Connection, + user: java.util.UUID, + forms: Seq[(java.util.UUID, ChangeForm)] + ): Unit = { + forms.map { case (guid, f) => toNamedParameter(user, guid, f) }.toList match { + case Nil => // no-op + case first :: rest => anorm.BatchSql(UpdateQuery.sql(), first, rest*).execute()(c) + } + } + + def delete( + user: java.util.UUID, + change: Change + ): Unit = { + db.withConnection { c => + delete(c, user, change) + } + } + + def delete( + c: java.sql.Connection, + user: java.util.UUID, + change: Change + ): Unit = { + deleteByGuid( + c = c, + user = user, + guid = change.guid + ) + } + + def deleteByGuid( + user: java.util.UUID, + guid: java.util.UUID + ): Unit = { + db.withConnection { c => + deleteByGuid(c, user, guid) + } + } + + def deleteByGuid( + c: java.sql.Connection, + user: java.util.UUID, + guid: java.util.UUID + ): Unit = { + DeleteQuery.equals("guid", guid) + .bind("deleted_at", org.joda.time.DateTime.now) + .bind("deleted_by_guid", user) + .execute(c) + } + + def deleteAllByGuids( + user: java.util.UUID, + guids: Seq[java.util.UUID] + ): Unit = { + db.withConnection { c => + deleteAllByGuids(c, user, guids) + } + } + + def deleteAllByGuids( + c: java.sql.Connection, + user: java.util.UUID, + guids: Seq[java.util.UUID] + ): Unit = { + DeleteQuery.in("guid", guids) + .bind("deleted_at", org.joda.time.DateTime.now) + .bind("deleted_by_guid", user) + .execute(c) + } + + def deleteAllByApplicationGuid( + user: java.util.UUID, + applicationGuid: java.util.UUID + ): Unit = { + db.withConnection { c => + deleteAllByApplicationGuid(c, user, applicationGuid) + } + } + + def deleteAllByApplicationGuid( + c: java.sql.Connection, + user: java.util.UUID, + applicationGuid: java.util.UUID + ): Unit = { + DeleteQuery.equals("application_guid", applicationGuid) + .bind("deleted_at", org.joda.time.DateTime.now) + .bind("deleted_by_guid", user) + .execute(c) + } + + def deleteAllByApplicationGuids( + user: java.util.UUID, + applicationGuids: Seq[java.util.UUID] + ): Unit = { + db.withConnection { c => + deleteAllByApplicationGuids(c, user, applicationGuids) + } + } + + def deleteAllByApplicationGuids( + c: java.sql.Connection, + user: java.util.UUID, + applicationGuids: Seq[java.util.UUID] + ): Unit = { + DeleteQuery.in("application_guid", applicationGuids) + .bind("deleted_at", org.joda.time.DateTime.now) + .bind("deleted_by_guid", user) + .execute(c) + } + + def deleteAllByToVersionGuid( + user: java.util.UUID, + toVersionGuid: java.util.UUID + ): Unit = { + db.withConnection { c => + deleteAllByToVersionGuid(c, user, toVersionGuid) + } + } + + def deleteAllByToVersionGuid( + c: java.sql.Connection, + user: java.util.UUID, + toVersionGuid: java.util.UUID + ): Unit = { + DeleteQuery.equals("to_version_guid", toVersionGuid) + .bind("deleted_at", org.joda.time.DateTime.now) + .bind("deleted_by_guid", user) + .execute(c) + } + + def deleteAllByToVersionGuids( + user: java.util.UUID, + toVersionGuids: Seq[java.util.UUID] + ): Unit = { + db.withConnection { c => + deleteAllByToVersionGuids(c, user, toVersionGuids) + } + } + + def deleteAllByToVersionGuids( + c: java.sql.Connection, + user: java.util.UUID, + toVersionGuids: Seq[java.util.UUID] + ): Unit = { + DeleteQuery.in("to_version_guid", toVersionGuids) + .bind("deleted_at", org.joda.time.DateTime.now) + .bind("deleted_by_guid", user) + .execute(c) + } + + private def bindQuery( + query: io.flow.postgresql.Query, + user: java.util.UUID, + form: ChangeForm + ): io.flow.postgresql.Query = { + query + .bind("application_guid", form.applicationGuid.toString) + .bind("from_version_guid", form.fromVersionGuid.toString) + .bind("to_version_guid", form.toVersionGuid.toString) + .bind("type", form.`type`) + .bind("description", form.description) + .bind("changed_at", form.changedAt) + .bind("changed_by_guid", form.changedByGuid.toString) + .bind("is_material", form.isMaterial) + .bind("updated_at", org.joda.time.DateTime.now) + } + + private def toNamedParameter( + user: java.util.UUID, + guid: java.util.UUID, + form: ChangeForm + ): Seq[anorm.NamedParameter] = { + Seq( + anorm.NamedParameter("guid", guid.toString), + anorm.NamedParameter("application_guid", form.applicationGuid.toString), + anorm.NamedParameter("from_version_guid", form.fromVersionGuid.toString), + anorm.NamedParameter("to_version_guid", form.toVersionGuid.toString), + anorm.NamedParameter("type", form.`type`), + anorm.NamedParameter("description", form.description), + anorm.NamedParameter("changed_at", form.changedAt), + anorm.NamedParameter("changed_by_guid", form.changedByGuid.toString), + anorm.NamedParameter("is_material", form.isMaterial), + anorm.NamedParameter("updated_at", org.joda.time.DateTime.now) + ) + } +} \ No newline at end of file