diff --git a/server/deploy/src/main/scala/cool/graph/deploy/database/persistence/DbToModelMapper.scala b/server/deploy/src/main/scala/cool/graph/deploy/database/persistence/DbToModelMapper.scala index 5129fad223..143884fce6 100644 --- a/server/deploy/src/main/scala/cool/graph/deploy/database/persistence/DbToModelMapper.scala +++ b/server/deploy/src/main/scala/cool/graph/deploy/database/persistence/DbToModelMapper.scala @@ -2,22 +2,12 @@ package cool.graph.deploy.database.persistence import cool.graph.deploy.database.tables.{Migration, Project} import cool.graph.shared.models -import cool.graph.shared.models.{MigrationStep, Schema, Function} +import cool.graph.shared.models.{MigrationStep, Schema} object DbToModelMapper { import cool.graph.shared.models.MigrationStepsJsonFormatter._ import cool.graph.shared.models.ProjectJsonFormatter._ -// def convert(migration: Migration): models.Project = { -// val projectModel = migration.schema.as[models.Project] -// projectModel.copy(revision = migration.revision) -// } - -// def convert(project: Project, migration: Migration): models.Project = { -// val projectModel = migration.schema.as[models.Project] -// projectModel.copy(revision = migration.revision) -// } - def convert(project: Project, migration: Migration): models.Project = { models.Project( id = project.id, @@ -42,7 +32,9 @@ object DbToModelMapper { applied = migration.applied, rolledBack = migration.rolledBack, steps = migration.steps.as[Vector[MigrationStep]], - errors = migration.errors.as[Vector[String]] + errors = migration.errors.as[Vector[String]], + startedAt = migration.startedAt, + finishedAt = migration.finishedAt ) } } diff --git a/server/deploy/src/main/scala/cool/graph/deploy/database/persistence/MigrationPersistence.scala b/server/deploy/src/main/scala/cool/graph/deploy/database/persistence/MigrationPersistence.scala index 4a82e0d8d0..094738bfcc 100644 --- a/server/deploy/src/main/scala/cool/graph/deploy/database/persistence/MigrationPersistence.scala +++ b/server/deploy/src/main/scala/cool/graph/deploy/database/persistence/MigrationPersistence.scala @@ -2,6 +2,7 @@ package cool.graph.deploy.database.persistence import cool.graph.shared.models.{Migration, MigrationId} import cool.graph.shared.models.MigrationStatus.MigrationStatus +import org.joda.time.DateTime import scala.concurrent.Future @@ -16,6 +17,8 @@ trait MigrationPersistence { def updateMigrationErrors(id: MigrationId, errors: Vector[String]): Future[Unit] def updateMigrationApplied(id: MigrationId, applied: Int): Future[Unit] def updateMigrationRolledBack(id: MigrationId, rolledBack: Int): Future[Unit] + def updateStartedAt(id: MigrationId, startedAt: DateTime): Future[Unit] + def updateFinishedAt(id: MigrationId, finishedAt: DateTime): Future[Unit] def loadDistinctUnmigratedProjectIds(): Future[Seq[String]] } diff --git a/server/deploy/src/main/scala/cool/graph/deploy/database/persistence/MigrationPersistenceImpl.scala b/server/deploy/src/main/scala/cool/graph/deploy/database/persistence/MigrationPersistenceImpl.scala index 894b906002..cc1d25941c 100644 --- a/server/deploy/src/main/scala/cool/graph/deploy/database/persistence/MigrationPersistenceImpl.scala +++ b/server/deploy/src/main/scala/cool/graph/deploy/database/persistence/MigrationPersistenceImpl.scala @@ -4,6 +4,7 @@ import cool.graph.deploy.database.tables.{MigrationTable, Tables} import cool.graph.shared.models.{Migration, MigrationId} import cool.graph.shared.models.MigrationStatus.MigrationStatus import cool.graph.utils.future.FutureUtils.FutureOpt +import org.joda.time.DateTime import play.api.libs.json.Json import slick.jdbc.MySQLProfile.api._ import slick.jdbc.MySQLProfile.backend.DatabaseDef @@ -62,6 +63,14 @@ case class MigrationPersistenceImpl( internalDatabase.run(MigrationTable.updateMigrationRolledBack(id.projectId, id.revision, rolledBack)).map(_ => ()) } + override def updateStartedAt(id: MigrationId, startedAt: DateTime): Future[Unit] = { + internalDatabase.run(MigrationTable.updateStartedAt(id.projectId, id.revision, startedAt)).map(_ => ()) + } + + override def updateFinishedAt(id: MigrationId, finishedAt: DateTime): Future[Unit] = { + internalDatabase.run(MigrationTable.updateFinishedAt(id.projectId, id.revision, finishedAt)).map(_ => ()) + } + override def getLastMigration(projectId: String): Future[Option[Migration]] = { FutureOpt(internalDatabase.run(MigrationTable.lastSuccessfulMigration(projectId))).map(DbToModelMapper.convert).future } diff --git a/server/deploy/src/main/scala/cool/graph/deploy/database/persistence/ModelToDbMapper.scala b/server/deploy/src/main/scala/cool/graph/deploy/database/persistence/ModelToDbMapper.scala index 1f0615c0c9..a6a096e7f1 100644 --- a/server/deploy/src/main/scala/cool/graph/deploy/database/persistence/ModelToDbMapper.scala +++ b/server/deploy/src/main/scala/cool/graph/deploy/database/persistence/ModelToDbMapper.scala @@ -38,7 +38,9 @@ object ModelToDbMapper { applied = migration.applied, rolledBack = migration.rolledBack, steps = migrationStepsJson, - errors = errorsJson + errors = errorsJson, + startedAt = migration.startedAt, + finishedAt = migration.finishedAt ) } } diff --git a/server/deploy/src/main/scala/cool/graph/deploy/database/schema/InternalDatabaseSchema.scala b/server/deploy/src/main/scala/cool/graph/deploy/database/schema/InternalDatabaseSchema.scala index f7b3affffc..e7f53b8804 100644 --- a/server/deploy/src/main/scala/cool/graph/deploy/database/schema/InternalDatabaseSchema.scala +++ b/server/deploy/src/main/scala/cool/graph/deploy/database/schema/InternalDatabaseSchema.scala @@ -42,6 +42,8 @@ object InternalDatabaseSchema { `rolledBack` int NOT NULL default 0, `steps` mediumtext COLLATE utf8_unicode_ci DEFAULT NULL, `errors` mediumtext COLLATE utf8_unicode_ci DEFAULT NULL, + `startedAt` datetime DEFAULT NULL, + `finishedAt` datetime DEFAULT NULL, PRIMARY KEY (`projectId`, `revision`), CONSTRAINT `migrations_projectid_foreign` FOREIGN KEY (`projectId`) REFERENCES `Project` (`id`) ON DELETE CASCADE ON UPDATE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;""", diff --git a/server/deploy/src/main/scala/cool/graph/deploy/database/tables/Migration.scala b/server/deploy/src/main/scala/cool/graph/deploy/database/tables/Migration.scala index 25cfd63641..0f8649ae75 100644 --- a/server/deploy/src/main/scala/cool/graph/deploy/database/tables/Migration.scala +++ b/server/deploy/src/main/scala/cool/graph/deploy/database/tables/Migration.scala @@ -1,7 +1,9 @@ package cool.graph.deploy.database.tables +import com.github.tototoshi.slick.MySQLJodaSupport import cool.graph.shared.models.MigrationStatus import cool.graph.shared.models.MigrationStatus.MigrationStatus +import org.joda.time.DateTime import play.api.libs.json.JsValue import slick.dbio.Effect.{Read, Write} import slick.jdbc.MySQLProfile.api._ @@ -16,12 +18,15 @@ case class Migration( applied: Int, rolledBack: Int, steps: JsValue, - errors: JsValue + errors: JsValue, + startedAt: Option[DateTime], + finishedAt: Option[DateTime] ) class MigrationTable(tag: Tag) extends Table[Migration](tag, "Migration") { implicit val statusMapper = MigrationTable.statusMapper implicit val jsonMapper = MigrationTable.jsonMapper + implicit val jodaMapper = MySQLJodaSupport.datetimeTypeMapper def projectId = column[String]("projectId") def revision = column[Int]("revision") @@ -32,13 +37,16 @@ class MigrationTable(tag: Tag) extends Table[Migration](tag, "Migration") { def rolledBack = column[Int]("rolledBack") def steps = column[JsValue]("steps") def errors = column[JsValue]("errors") + def startedAt = column[Option[DateTime]]("startedAt") + def finishedAt = column[Option[DateTime]]("finishedAt") def migration = foreignKey("migrations_projectid_foreign", projectId, Tables.Projects)(_.id) - def * = (projectId, revision, schema, functions, status, applied, rolledBack, steps, errors) <> (Migration.tupled, Migration.unapply) + def * = (projectId, revision, schema, functions, status, applied, rolledBack, steps, errors, startedAt, finishedAt) <> (Migration.tupled, Migration.unapply) } object MigrationTable { implicit val jsonMapper = MappedColumns.jsonMapper + implicit val jodaMapper = MySQLJodaSupport.datetimeTypeMapper implicit val statusMapper = MappedColumnType.base[MigrationStatus, String]( _.toString, MigrationStatus.withName @@ -100,6 +108,14 @@ object MigrationTable { updateBaseQuery(projectId, revision).map(_.rolledBack).update(rolledBack) } + def updateStartedAt(projectId: String, revision: Int, startedAt: DateTime) = { + updateBaseQuery(projectId, revision).map(_.startedAt).update(Some(startedAt)) + } + + def updateFinishedAt(projectId: String, revision: Int, finishedAt: DateTime) = { + updateBaseQuery(projectId, revision).map(_.finishedAt).update(Some(finishedAt)) + } + def loadByRevision(projectId: String, revision: Int): SqlAction[Option[Migration], NoStream, Read] = { val baseQuery = for { migration <- Tables.Migrations diff --git a/server/deploy/src/main/scala/cool/graph/deploy/migration/migrator/MigrationApplier.scala b/server/deploy/src/main/scala/cool/graph/deploy/migration/migrator/MigrationApplier.scala index e7dc5c7e39..025f9c7e52 100644 --- a/server/deploy/src/main/scala/cool/graph/deploy/migration/migrator/MigrationApplier.scala +++ b/server/deploy/src/main/scala/cool/graph/deploy/migration/migrator/MigrationApplier.scala @@ -5,6 +5,7 @@ import cool.graph.deploy.migration.MigrationStepMapper import cool.graph.deploy.migration.mutactions.ClientSqlMutaction import cool.graph.shared.models.{Migration, MigrationStatus, MigrationStep, Schema} import cool.graph.utils.exceptions.StackTraceUtils +import org.joda.time.DateTime import slick.jdbc.MySQLProfile.backend.DatabaseDef import scala.concurrent.{ExecutionContext, Future} @@ -27,7 +28,9 @@ case class MigrationApplierImpl( _ <- Future.unit nextState = if (migration.status == MigrationStatus.Pending) MigrationStatus.InProgress else migration.status _ <- migrationPersistence.updateMigrationStatus(migration.id, nextState) + _ <- migrationPersistence.updateStartedAt(migration.id, DateTime.now()) result <- startRecurse(previousSchema, migration) + _ <- migrationPersistence.updateFinishedAt(migration.id, DateTime.now()) } yield result } diff --git a/server/deploy/src/main/scala/cool/graph/deploy/schema/CustomScalarTypes.scala b/server/deploy/src/main/scala/cool/graph/deploy/schema/CustomScalarTypes.scala new file mode 100644 index 0000000000..cd9f5746f5 --- /dev/null +++ b/server/deploy/src/main/scala/cool/graph/deploy/schema/CustomScalarTypes.scala @@ -0,0 +1,34 @@ +package cool.graph.deploy.schema + +import org.joda.time.{DateTime, DateTimeZone} +import sangria.ast +import sangria.schema.ScalarType +import sangria.validation.ValueCoercionViolation + +import scala.util.{Failure, Success, Try} + +object CustomScalarTypes { + case object DateCoercionViolation extends ValueCoercionViolation("Date value expected") + + def parseDate(s: String) = Try(new DateTime(s, DateTimeZone.UTC)) match { + case Success(date) ⇒ Right(date) + case Failure(_) ⇒ Left(DateCoercionViolation) + } + + val DateTimeType = + ScalarType[DateTime]( + "DateTime", + coerceOutput = (d, caps) => { + d.toDateTime + }, + coerceUserInput = { + case s: String ⇒ parseDate(s) + case _ ⇒ Left(DateCoercionViolation) + }, + coerceInput = { + case ast.StringValue(s, _, _, _, _) ⇒ parseDate(s) + case _ ⇒ Left(DateCoercionViolation) + } + ) + +} diff --git a/server/deploy/src/main/scala/cool/graph/deploy/schema/types/MigrationType.scala b/server/deploy/src/main/scala/cool/graph/deploy/schema/types/MigrationType.scala index 57b0612f34..6296c41f30 100644 --- a/server/deploy/src/main/scala/cool/graph/deploy/schema/types/MigrationType.scala +++ b/server/deploy/src/main/scala/cool/graph/deploy/schema/types/MigrationType.scala @@ -1,6 +1,6 @@ package cool.graph.deploy.schema.types -import cool.graph.deploy.schema.SystemUserContext +import cool.graph.deploy.schema.{CustomScalarTypes, SystemUserContext} import cool.graph.shared.models import sangria.schema._ @@ -15,7 +15,9 @@ object MigrationType { Field("applied", IntType, resolve = _.value.applied), Field("rolledBack", IntType, resolve = _.value.rolledBack), Field("steps", ListType(MigrationStepType.Type), resolve = _.value.steps), - Field("errors", ListType(StringType), resolve = _.value.errors) + Field("errors", ListType(StringType), resolve = _.value.errors), + Field("startedAt", OptionType(CustomScalarTypes.DateTimeType), resolve = _.value.startedAt), + Field("finishedAt", OptionType(CustomScalarTypes.DateTimeType), resolve = _.value.finishedAt) ) ) } diff --git a/server/deploy/src/test/scala/cool/graph/deploy/database/persistence/MigrationPersistenceImplSpec.scala b/server/deploy/src/test/scala/cool/graph/deploy/database/persistence/MigrationPersistenceImplSpec.scala index f4c1fc3c3b..a0cef41ce8 100644 --- a/server/deploy/src/test/scala/cool/graph/deploy/database/persistence/MigrationPersistenceImplSpec.scala +++ b/server/deploy/src/test/scala/cool/graph/deploy/database/persistence/MigrationPersistenceImplSpec.scala @@ -3,6 +3,7 @@ package cool.graph.deploy.database.persistence import cool.graph.deploy.database.tables.Tables import cool.graph.deploy.specutils.{DeploySpecBase, TestProject} import cool.graph.shared.models._ +import org.joda.time.DateTime import org.scalatest.{FlatSpec, Matchers} import slick.jdbc.MySQLProfile.api._ @@ -110,6 +111,28 @@ class MigrationPersistenceImplSpec extends FlatSpec with Matchers with DeploySpe reloadedMigration.rolledBack shouldEqual 1 } + ".updateMigrationStartedAt()" should "update the migration startedAt timestamp correctly" in { + val (project, _) = setupProject(basicTypesGql) + val createdMigration = migrationPersistence.create(Migration.empty(project.id)).await + val time = DateTime.now() + + migrationPersistence.updateStartedAt(createdMigration.id, time).await + + val reloadedMigration = migrationPersistence.byId(createdMigration.id).await.get + reloadedMigration.startedAt.isDefined shouldEqual true // some bug causes mysql timstamps to be off by a margin, equal is broken + } + + ".updateMigrationFinishedAt()" should "update the migration finishedAt timestamp correctly" in { + val (project, _) = setupProject(basicTypesGql) + val createdMigration = migrationPersistence.create(Migration.empty(project.id)).await + val time = DateTime.now() + + migrationPersistence.updateFinishedAt(createdMigration.id, time).await + + val reloadedMigration = migrationPersistence.byId(createdMigration.id).await.get + reloadedMigration.finishedAt.isDefined shouldEqual true // some bug causes mysql timstamps to be off by a margin, equal is broken + } + ".getLastMigration()" should "get the last migration applied to a project" in { val (project, _) = setupProject(basicTypesGql) migrationPersistence.getLastMigration(project.id).await.get.revision shouldEqual 2 diff --git a/server/deploy/src/test/scala/cool/graph/deploy/migration/migrator/MigrationApplierSpec.scala b/server/deploy/src/test/scala/cool/graph/deploy/migration/migrator/MigrationApplierSpec.scala index c48b7e14a2..36fcbc8ef4 100644 --- a/server/deploy/src/test/scala/cool/graph/deploy/migration/migrator/MigrationApplierSpec.scala +++ b/server/deploy/src/test/scala/cool/graph/deploy/migration/migrator/MigrationApplierSpec.scala @@ -44,6 +44,8 @@ class MigrationApplierSpec extends FlatSpec with Matchers with DeploySpecBase wi persisted.status should be(MigrationStatus.Success) persisted.applied should be(migration.steps.size) persisted.rolledBack should be(0) + persisted.startedAt.isDefined shouldEqual true + persisted.finishedAt.isDefined shouldEqual true } "the applier" should "mark a migration as ROLLBACK_SUCCESS if all steps can be rolled back successfully" in { diff --git a/server/shared-models/src/main/scala/cool/graph/shared/models/Migration.scala b/server/shared-models/src/main/scala/cool/graph/shared/models/Migration.scala index 8149eebdd0..52b3389278 100644 --- a/server/shared-models/src/main/scala/cool/graph/shared/models/Migration.scala +++ b/server/shared-models/src/main/scala/cool/graph/shared/models/Migration.scala @@ -1,12 +1,7 @@ package cool.graph.shared.models import cool.graph.shared.models.MigrationStatus.MigrationStatus - -//case class UnappliedMigration( -// previousProject: Project, -// nextProject: Project, -// migration: Migration -//) +import org.joda.time.DateTime case class MigrationId(projectId: String, revision: Int) @@ -19,7 +14,9 @@ case class Migration( applied: Int, rolledBack: Int, steps: Vector[MigrationStep], - errors: Vector[String] + errors: Vector[String], + startedAt: Option[DateTime] = None, + finishedAt: Option[DateTime] = None ) { def id: MigrationId = MigrationId(projectId, revision) def isRollingBack: Boolean = status == MigrationStatus.RollingBack