app/services/editions/db/IssueQueries.scala (380 lines of code) (raw):
package services.editions.db
import java.time.format.DateTimeFormatter
import java.time.{LocalDate, OffsetDateTime, ZoneId}
import com.gu.pandomainauth.model.User
import logging.Logging
import model.editions._
import org.postgresql.util.PSQLException
import play.api.libs.json.Json
import scalikejdbc._
import services.editions.publishing.events.PublishEvent
import services.editions.{
DbEditionsCard,
DbEditionsCollection,
DbEditionsFront,
GenerateEditionTemplateResult
}
import scala.util.{Failure, Success, Try}
trait IssueQueries extends Logging {
def insertIssue(
edition: Edition,
issueSkeleton: EditionsIssueSkeleton,
user: User,
now: OffsetDateTime
): String = DB localTx { implicit session =>
val truncatedNow = EditionsDB.truncateDateTime(now)
val userName = EditionsDB.getUserName(user)
val issueId =
sql"""
INSERT INTO edition_issues (
name,
issue_date,
timezone_id,
created_on,
created_by,
created_email
) VALUES (${edition.entryName}, ${issueSkeleton.issueDate}, ${issueSkeleton.zoneId.toString}, $truncatedNow, $userName, ${user.email})
RETURNING id;
""".map(_.string("id")).single.apply().get
issueSkeleton.fronts.zipWithIndex.foreach { case (front, fIndex) =>
val frontId =
sql"""
INSERT INTO fronts (
issue_id,
index,
name,
is_hidden,
metadata,
is_special
) VALUES ($issueId, $fIndex, ${front.name}, ${front.hidden}, ${front
.metadata()}, ${front.isSpecial})
RETURNING id;
""".map(_.string("id")).single.apply().get
front.collections.zipWithIndex.foreach { case (collection, cIndex) =>
val collectionId =
sql"""
INSERT INTO collections (
front_id,
index,
name,
is_hidden,
metadata,
prefill,
path_type,
content_prefill_window_start,
content_prefill_window_end,
updated_on,
updated_by,
updated_email
) VALUES (
$frontId
, $cIndex
, ${collection.name}
, ${collection.hidden}
, NULL
, ${collection.prefill.map(_.queryString)}
, ${collection.prefill.map(_.pathType.entryName)}
, ${collection.capiQueryTimeWindow.fromDate}
, ${collection.capiQueryTimeWindow.toDate}
, $truncatedNow
, $userName
, ${user.email}
)
RETURNING id;
""".map(_.string("id")).single.apply().get
collection.items.zipWithIndex.foreach { case (card, tIndex) =>
sql"""
INSERT INTO cards (
collection_id,
id,
card_type,
index,
added_on,
metadata
) VALUES ($collectionId, ${card.id}, ${card.cardType.entryName}, $tIndex, $truncatedNow, ${card.metadata
.map(Json.toJson(_).toString)
.getOrElse("{}")}::JSONB)
""".execute.apply()
}
}
}
issueId
}
def insertIssueFromClosestPreviousIssue(
edition: Edition,
issueDate: LocalDate,
user: User,
now: OffsetDateTime
): Either[Error, EditionsIssue] = DB readOnly { implicit session =>
for {
previousIssue <- getClosestPreviousIssue(issueDate, edition).toRight(
EditionsDB.NotFoundError(s"Previous issue not found")
)
newIssueSkeleton = previousIssue.toSkeleton.copy(issueDate = issueDate)
issueId = insertIssue(edition, newIssueSkeleton, user, now)
issue <- getIssue(issueId).toRight(
EditionsDB.NotFoundError(
"Issue created but could not retrieve it from the database"
)
)
} yield issue
}
private def getClosestPreviousIssue(issueDate: LocalDate, edition: Edition)(
implicit session: DBSession
): Option[EditionsIssue] = {
val id =
sql"""
SELECT id from edition_issues
WHERE issue_date < $issueDate
AND name=${edition.entryName}
ORDER BY issue_date DESC
LIMIT 1
""".map(_.string("id")).single.apply()
id.flatMap(getIssue)
}
def listIssues(edition: Edition, dateFrom: LocalDate, dateTo: LocalDate) =
DB readOnly { implicit session =>
val maybeIssues = sql"""
SELECT
id,
name,
issue_date,
timezone_id,
created_on,
created_by,
created_email,
launched_on,
launched_by,
launched_email
FROM edition_issues
WHERE issue_date BETWEEN $dateFrom AND $dateTo AND name = ${edition.entryName}
""".map(EditionsIssue.fromRow(_)).list.apply()
maybeIssues.partitionMap(identity) match {
case (Nil, rights) => Right(rights)
case (lefts, _) => Left(lefts)
}
}
def getIssueIdFromCollectionId(collectionId: String): Option[String] =
DB readOnly { implicit session =>
sql"""
SELECT edition_issues.id AS id
FROM edition_issues
INNER JOIN fronts ON (fronts.issue_id = edition_issues.id)
INNER JOIN collections ON (collections.front_id = fronts.id)
WHERE collections.id = $collectionId
""".map { rs =>
rs.string("id")
}.toOption
.apply()
}
def getIssue(edition: Edition, date: LocalDate): Option[EditionsIssue] =
DB readOnly { implicit session =>
getIssue(
sqls"""WHERE edition_issues.name = ${edition.entryName} AND edition_issues.issue_date = $date"""
)
}
def getIssue(id: String): Option[EditionsIssue] = DB readOnly {
implicit session =>
getIssue(sqls"""WHERE edition_issues.id = $id""")
}
private def getIssue(
whereClause: SQLSyntax
)(implicit session: DBSession): Option[EditionsIssue] = {
val rows: List[(EditionsIssue, FrontAndNestedEntitiesRow)] =
sql"""
SELECT
edition_issues.id,
edition_issues.name,
edition_issues.timezone_id,
edition_issues.issue_date,
edition_issues.created_on,
edition_issues.created_by,
edition_issues.created_email,
edition_issues.launched_on,
edition_issues.launched_by,
edition_issues.launched_email,
${FrontsQueries.frontAndNestedEntitiesColumns}
FROM edition_issues
LEFT JOIN fronts ON (fronts.issue_id = edition_issues.id)
LEFT JOIN collections ON (collections.front_id = fronts.id)
LEFT JOIN cards ON (cards.collection_id = collections.id)
$whereClause
"""
.map { rs =>
val maybeIssue = EditionsIssue.fromRow(rs).toOption
val frontRow = FrontAndNestedEntitiesRow(
DbEditionsFront.fromRowOpt(rs, "fronts_"),
DbEditionsCollection.fromRowOpt(rs, "collections_"),
DbEditionsCard.fromRowOpt(rs, "cards_")
)
maybeIssue.map(issue => (issue, frontRow))
}
.list
.apply()
.flatten
rows.headOption.map { case (issue, _) =>
issue.copy(fronts = FrontsQueries.toEditionsFront(rows.map(_._2)))
}
}
def getIssueSummary(id: String): Option[Either[String, EditionsIssue]] =
DB readOnly { implicit session =>
sql"""
SELECT
edition_issues.id,
edition_issues.name,
edition_issues.timezone_id,
edition_issues.issue_date,
edition_issues.created_on,
edition_issues.created_by,
edition_issues.created_email,
edition_issues.launched_on,
edition_issues.launched_by,
edition_issues.launched_email
FROM edition_issues
WHERE edition_issues.id = $id
"""
.map(rs => EditionsIssue.fromRow(rs))
.single
.apply()
}
def createIssueVersion(
issueId: String,
user: User,
now: OffsetDateTime
): EditionIssueVersionId = DB localTx { implicit session =>
val userName = user.firstName + " " + user.lastName
val truncatedNow = EditionsDB.truncateDateTime(now)
// versionId is a date string but everything downstream treats it as a string
// until we get back to the fronts tool
val versionId: String = now.format(DateTimeFormatter.ISO_DATE_TIME)
sql"""
UPDATE edition_issues
SET launched_on = $truncatedNow,
launched_by = $userName,
launched_email = ${user.email}
WHERE id = $issueId
""".execute.apply()
sql"""
INSERT INTO issue_versions (
id
, issue_id
, launched_on
, launched_by
, launched_email
) VALUES (
$versionId
, $issueId
, $truncatedNow
, $userName
, ${user.email}
);
""".execute.apply()
sql"""
INSERT INTO issue_versions_events (
version_id
, event_time
, status
) VALUES (
$versionId
, $truncatedNow
, ${IssueVersionStatus.Started.toString}
)
RETURNING version_id;
""".map(_.string("version_id")).single.apply().get
}
def deleteIssue(issueId: String) = DB localTx { implicit session =>
sql"""
DELETE FROM edition_issues
WHERE id = $issueId
""".execute.apply()
}
def getLastProofedIssueVersion(
issueId: String
): Option[EditionIssueVersionId] = DB localTx { implicit session =>
sql"""
SELECT max(v.id) AS version_id
FROM issue_versions v
LEFT JOIN issue_versions_events e
ON v.id = e.version_id
WHERE v.issue_id = $issueId
AND e.status = ${IssueVersionStatus.Proofed.toString}
"""
.map(rs => rs.string("version_id"))
.list
.apply()
.headOption
}
def getIssueVersions(issueId: String): List[IssueVersion] = DB localTx {
implicit session =>
case class Row(version: IssueVersion, event: IssueVersionEvent)
val rows: List[Row] =
sql"""
SELECT
v.id AS version_id
, v.launched_on AS version_launched_on
, v.launched_by AS version_launched_by
, v.launched_email AS version_launched_email
, e.event_time AS event_time
, e.status AS event_status
, e.message AS event_message
FROM issue_versions v
LEFT JOIN issue_versions_events e
ON v.id = e.version_id
WHERE v.issue_id = $issueId
ORDER BY launched_on DESC
""".map(rs => Row(IssueVersion.fromRow(rs), IssueVersionEvent.fromRow(rs)))
.list
.apply()
(rows.groupBy(_.version) map { case (version, rows) =>
version.copy(events = rows.map(_.event).sortBy(_.eventTime))
}).toList
.sortBy(_.launchedOn)
.reverse
}
def insertIssueVersionEvent(event: PublishEvent) = DB localTx {
implicit session =>
Try {
logger.info(s"saving issue version event message:${event.message}")(
event.toLogMarker
)
sql"""
INSERT INTO issue_versions_events (
version_id
, event_time
, status
, message
)
VALUES (
${event.version}
, ${event.timestamp}
, ${event.status.toString}
, ${event.message}
);
""".execute.apply()
} match {
case Success(_) => {
logger.info(
s"successfully inserted issue version event message:${event.message}"
)(event.toLogMarker)
true
}
case Failure(exception: PSQLException)
if exception.getSQLState == ForeignKeyViolationSQLState => {
logger.warn(
"Foreign key constraint violation encountered when inserting issue version event"
)(event.toLogMarker)
true
}
case Failure(exception: PSQLException) => {
logger.warn(
s"Postgres exception (${exception.getMessage}) encountered when inserting issue version event"
)(event.toLogMarker)
true
}
case Failure(exception) => {
logger.warn(
"Non-database exception (${exception.getMessage}) encountered when inserting issue version event"
)(event.toLogMarker)
true
}
}
}
}