From e22a94604d6090d88801ec52c39f4eab500e80e1 Mon Sep 17 00:00:00 2001 From: Jakob Odersky Date: Mon, 24 Jul 2017 16:24:12 -0700 Subject: Implement ReP rest services --- build.sbt | 2 +- .../xyz/driver/pdsuidomain/entities/Document.scala | 22 ++-- .../pdsuidomain/entities/MedicalRecord.scala | 16 ++- .../formats/json/document/ApiDocument.scala | 42 ++++++-- .../formats/json/document/ApiDocumentType.scala | 10 +- .../formats/json/document/ApiPartialDocument.scala | 28 +++-- .../formats/json/document/ApiProviderType.scala | 10 +- .../json/export/ApiExportPatientLabel.scala | 10 +- .../export/ApiExportPatientLabelEvidence.scala | 14 ++- .../ApiExportPatientLabelEvidenceDocument.scala | 15 ++- .../json/export/ApiExportPatientWithLabels.scala | 11 +- .../json/extracteddata/ApiExtractedData.scala | 17 ++- .../formats/json/record/ApiCreateRecord.scala | 3 +- .../formats/json/record/ApiRecord.scala | 37 ++++++- .../services/MedicalRecordService.scala | 33 ++---- .../services/rest/RestDocumentService.scala | 111 +++++++++++++++++++ .../services/rest/RestDocumentTypeService.scala | 32 ++++++ .../services/rest/RestExtractedDataService.scala | 97 +++++++++++++++++ .../services/rest/RestMedicalRecordService.scala | 119 +++++++++++++++++++++ .../services/rest/RestMessageService.scala | 2 +- .../services/rest/RestProviderTypeService.scala | 33 ++++++ 21 files changed, 596 insertions(+), 68 deletions(-) create mode 100644 src/main/scala/xyz/driver/pdsuidomain/services/rest/RestDocumentService.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/services/rest/RestDocumentTypeService.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/services/rest/RestExtractedDataService.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/services/rest/RestMedicalRecordService.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/services/rest/RestProviderTypeService.scala diff --git a/build.sbt b/build.sbt index bd5450d..fe6b2b4 100644 --- a/build.sbt +++ b/build.sbt @@ -17,7 +17,7 @@ lazy val core = (project in file(".")) "io.github.cloudify" %% "spdf" % "1.4.0", "org.davidbild" %% "tristate-core" % "0.2.0", "org.davidbild" %% "tristate-play" % "0.2.0" exclude ("com.typesafe.play", "play-json"), - "xyz.driver" %% "core" % "0.13.22", + "xyz.driver" %% "core" % "0.14.0", "xyz.driver" %% "domain-model" % "0.11.5", "ch.qos.logback" % "logback-classic" % "1.1.7", "com.fasterxml.jackson.datatype" % "jackson-datatype-jsr310" % "2.8.4", diff --git a/src/main/scala/xyz/driver/pdsuidomain/entities/Document.scala b/src/main/scala/xyz/driver/pdsuidomain/entities/Document.scala index d324fcd..1f73184 100644 --- a/src/main/scala/xyz/driver/pdsuidomain/entities/Document.scala +++ b/src/main/scala/xyz/driver/pdsuidomain/entities/Document.scala @@ -181,13 +181,14 @@ object Document { val All = Set[Status](New, Organized, Extracted, Done, Flagged, Archived) val AllPrevious = Set[Status](Organized, Extracted) - val fromString: PartialFunction[String, Status] = { - case "New" => Status.New - case "Organized" => Status.Organized - case "Extracted" => Status.Extracted - case "Done" => Status.Done - case "Flagged" => Status.Flagged - case "Archived" => Status.Archived + def fromString(status: String): Option[Status] = status match { + case "New" => Some(Status.New) + case "Organized" => Some(Status.Organized) + case "Extracted" => Some(Status.Extracted) + case "Done" => Some(Status.Done) + case "Flagged" => Some(Status.Flagged) + case "Archived" => Some(Status.Archived) + case _ => None } def statusToString(x: Status): String = x match { @@ -216,9 +217,10 @@ object Document { val All = Set[RequiredType](OPN, PN) - val fromString: PartialFunction[String, RequiredType] = { - case "OPN" => RequiredType.OPN - case "PN" => RequiredType.PN + def fromString(tpe: String): Option[RequiredType] = tpe match { + case "OPN" => Some(RequiredType.OPN) + case "PN" => Some(RequiredType.PN) + case _ => None } def requiredTypeToString(x: RequiredType): String = x match { diff --git a/src/main/scala/xyz/driver/pdsuidomain/entities/MedicalRecord.scala b/src/main/scala/xyz/driver/pdsuidomain/entities/MedicalRecord.scala index 3b53945..9b33af4 100644 --- a/src/main/scala/xyz/driver/pdsuidomain/entities/MedicalRecord.scala +++ b/src/main/scala/xyz/driver/pdsuidomain/entities/MedicalRecord.scala @@ -104,6 +104,21 @@ object MedicalRecord { case object Flagged extends Status case object Archived extends Status + def fromString(status: String): Option[Status] = status match { + case "Unprocessed" => Some(Unprocessed) + case "PreCleaning" => Some(PreCleaning) + case "New" => Some(New) + case "Cleaned" => Some(Cleaned) + case "PreOrganized" => Some(PreOrganized) + case "PreOrganizing" => Some(PreOrganizing) + case "Reviewed" => Some(Reviewed) + case "Organized" => Some(Organized) + case "Done" => Some(Done) + case "Flagged" => Some(Flagged) + case "Archived" => Some(Archived) + case _ => None + } + val All = Set[Status]( Unprocessed, PreCleaning, @@ -150,7 +165,6 @@ final case class MedicalRecord(id: LongId[MedicalRecord], disease: String, caseId: Option[CaseId], physician: Option[String], - sourceName: String, meta: Option[TextJson[List[Meta]]], predictedMeta: Option[TextJson[List[Meta]]], predictedDocuments: Option[TextJson[List[Document]]], diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/document/ApiDocument.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/document/ApiDocument.scala index c01e65c..1869ff3 100644 --- a/src/main/scala/xyz/driver/pdsuidomain/formats/json/document/ApiDocument.scala +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/document/ApiDocument.scala @@ -1,6 +1,8 @@ package xyz.driver.pdsuidomain.formats.json.document import java.time.{LocalDate, ZoneId, ZonedDateTime} +import xyz.driver.pdsuicommon.domain.{LongId, StringId, TextJson} +import xyz.driver.pdsuicommon.json.JsonSerializer import xyz.driver.pdsuidomain.entities._ import play.api.data.validation.ValidationError @@ -11,7 +13,7 @@ import xyz.driver.pdsuicommon.json.JsonSerializer final case class ApiDocument(id: Long, recordId: Long, physician: Option[String], - lastUpdate: Option[ZonedDateTime], + lastUpdate: ZonedDateTime, typeId: Option[Long], startDate: Option[LocalDate], endDate: Option[LocalDate], @@ -23,14 +25,40 @@ final case class ApiDocument(id: Long, assignee: Option[String], previousAssignee: Option[String], lastActiveUser: Option[String], - meta: Option[String]) + meta: Option[String]) { + + private def extractStatus(status: String): Document.Status = + Document.Status.fromString(status).getOrElse(throw new NoSuchElementException(s"Status $status unknown")) + + private def extractRequiredType(tpe: String): Document.RequiredType = + Document.RequiredType.fromString(tpe).getOrElse(throw new NoSuchElementException(s"RequitedType $tpe unknown")) + + def toDomain = Document( + id = LongId(this.id), + status = extractStatus(this.status.getOrElse("")), + previousStatus = previousStatus.map(extractStatus), + assignee = this.assignee.map(StringId(_)), + previousAssignee = this.previousAssignee.map(StringId(_)), + lastActiveUserId = this.lastActiveUser.map(StringId(_)), + recordId = LongId(this.recordId), + physician = this.physician, + typeId = this.typeId.map(LongId(_)), + providerName = this.provider, + providerTypeId = this.providerTypeId.map(LongId(_)), + requiredType = this.requiredType.map(extractRequiredType), + meta = this.meta.map(x => TextJson(JsonSerializer.deserialize[Document.Meta](x))), + startDate = this.startDate, + endDate = this.endDate, + lastUpdate = this.lastUpdate.toLocalDateTime() + ) + +} object ApiDocument { private val statusFormat = Format( - Reads.StringReads.filter(ValidationError("unknown status")) { - case x if Document.Status.fromString.isDefinedAt(x) => true - case _ => false + Reads.StringReads.filter(ValidationError("unknown status")) { x => + Document.Status.fromString(x).isDefined }, Writes.StringWrites ) @@ -39,7 +67,7 @@ object ApiDocument { (JsPath \ "id").format[Long] and (JsPath \ "recordId").format[Long] and (JsPath \ "physician").formatNullable[String] and - (JsPath \ "lastUpdate").formatNullable[ZonedDateTime] and + (JsPath \ "lastUpdate").format[ZonedDateTime] and (JsPath \ "typeId").formatNullable[Long] and (JsPath \ "startDate").formatNullable[LocalDate] and (JsPath \ "endDate").formatNullable[LocalDate] and @@ -61,7 +89,7 @@ object ApiDocument { id = document.id.id, recordId = document.recordId.id, physician = document.physician, - lastUpdate = Option(document.lastUpdate).map(ZonedDateTime.of(_, ZoneId.of("Z"))), + lastUpdate = ZonedDateTime.of(document.lastUpdate, ZoneId.of("Z")), typeId = document.typeId.map(_.id), startDate = document.startDate, endDate = document.endDate, diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/document/ApiDocumentType.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/document/ApiDocumentType.scala index 8b11b91..8b4974f 100644 --- a/src/main/scala/xyz/driver/pdsuidomain/formats/json/document/ApiDocumentType.scala +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/document/ApiDocumentType.scala @@ -2,9 +2,17 @@ package xyz.driver.pdsuidomain.formats.json.document import play.api.libs.functional.syntax._ import play.api.libs.json.{Format, JsPath} +import xyz.driver.pdsuicommon.domain.LongId import xyz.driver.pdsuidomain.entities.DocumentType -final case class ApiDocumentType(id: Long, name: String) +final case class ApiDocumentType(id: Long, name: String) { + + def toDomain = DocumentType( + id = LongId(this.id), + name = this.name + ) + +} object ApiDocumentType { diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/document/ApiPartialDocument.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/document/ApiPartialDocument.scala index e9485e7..eae0c62 100644 --- a/src/main/scala/xyz/driver/pdsuidomain/formats/json/document/ApiPartialDocument.scala +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/document/ApiPartialDocument.scala @@ -31,7 +31,7 @@ final case class ApiPartialDocument(recordId: Option[Long], def applyTo(orig: Document): Document = Document( id = orig.id, - status = status.map(Document.Status.fromString).getOrElse(orig.status), + status = status.flatMap(Document.Status.fromString).getOrElse(orig.status), previousStatus = orig.previousStatus, assignee = assignee.map(StringId[User]).cata(Some(_), None, orig.assignee), previousAssignee = orig.previousAssignee, @@ -90,16 +90,22 @@ object ApiPartialDocument { (JsPath \ "endDate").readTristate[LocalDate] and (JsPath \ "provider").readTristate[String] and (JsPath \ "providerTypeId").readTristate[Long] and - (JsPath \ "status").readNullable[String](Reads.of[String].filter(ValidationError("unknown status"))({ - case x if Document.Status.fromString.isDefinedAt(x) => true - case _ => false - })) and + (JsPath \ "status").readNullable[String]( + Reads + .of[String] + .filter(ValidationError("unknown status"))( + Document.Status.fromString(_).isDefined + )) and (JsPath \ "assignee").readTristate[String] and - (JsPath \ "meta").readTristate(Reads { x => JsSuccess(Json.stringify(x)) }).map { - case Tristate.Present("{}") => Tristate.Absent - case x => x - } - ) (ApiPartialDocument.apply _) + (JsPath \ "meta") + .readTristate(Reads { x => + JsSuccess(Json.stringify(x)) + }) + .map { + case Tristate.Present("{}") => Tristate.Absent + case x => x + } + )(ApiPartialDocument.apply _) private val writes: Writes[ApiPartialDocument] = ( (JsPath \ "recordId").writeNullable[Long] and @@ -112,7 +118,7 @@ object ApiPartialDocument { (JsPath \ "status").writeNullable[String] and (JsPath \ "assignee").writeTristate[String] and (JsPath \ "meta").writeTristate(Writes[String](Json.parse)) - ) (unlift(ApiPartialDocument.unapply)) + )(unlift(ApiPartialDocument.unapply)) implicit val format: Format[ApiPartialDocument] = Format(reads, writes) } diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/document/ApiProviderType.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/document/ApiProviderType.scala index eb0ac46..c0eddad 100644 --- a/src/main/scala/xyz/driver/pdsuidomain/formats/json/document/ApiProviderType.scala +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/document/ApiProviderType.scala @@ -2,9 +2,17 @@ package xyz.driver.pdsuidomain.formats.json.document import play.api.libs.functional.syntax._ import play.api.libs.json.{Format, JsPath} +import xyz.driver.pdsuicommon.domain.LongId import xyz.driver.pdsuidomain.entities.ProviderType -final case class ApiProviderType(id: Long, name: String) +final case class ApiProviderType(id: Long, name: String) { + + def toDomain = ProviderType( + id = LongId(this.id), + name = this.name + ) + +} object ApiProviderType { diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/export/ApiExportPatientLabel.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/export/ApiExportPatientLabel.scala index 1d5a171..0ef1c68 100644 --- a/src/main/scala/xyz/driver/pdsuidomain/formats/json/export/ApiExportPatientLabel.scala +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/export/ApiExportPatientLabel.scala @@ -2,9 +2,17 @@ package xyz.driver.pdsuidomain.formats.json.export import play.api.libs.functional.syntax._ import play.api.libs.json.{Format, JsPath} +import xyz.driver.pdsuicommon.domain.LongId import xyz.driver.pdsuidomain.entities.export.patient.ExportPatientLabel -final case class ApiExportPatientLabel(id: String, evidences: List[ApiExportPatientLabelEvidence]) +final case class ApiExportPatientLabel(id: String, evidences: List[ApiExportPatientLabelEvidence]) { + + def toDomain = ExportPatientLabel( + id = LongId(this.id.toLong), + evidences = this.evidences.map(_.toDomain) + ) + +} object ApiExportPatientLabel { diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/export/ApiExportPatientLabelEvidence.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/export/ApiExportPatientLabelEvidence.scala index 9ce281e..d141762 100644 --- a/src/main/scala/xyz/driver/pdsuidomain/formats/json/export/ApiExportPatientLabelEvidence.scala +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/export/ApiExportPatientLabelEvidence.scala @@ -2,13 +2,23 @@ package xyz.driver.pdsuidomain.formats.json.export import play.api.libs.functional.syntax._ import play.api.libs.json._ -import xyz.driver.pdsuicommon.domain.FuzzyValue +import xyz.driver.pdsuicommon.domain.{FuzzyValue, LongId} import xyz.driver.pdsuidomain.entities.export.patient.ExportPatientLabelEvidence final case class ApiExportPatientLabelEvidence(evidenceId: String, labelValue: String, evidenceText: String, - document: ApiExportPatientLabelEvidenceDocument) + document: ApiExportPatientLabelEvidenceDocument) { + + def toDomain = ExportPatientLabelEvidence( + id = LongId(this.evidenceId.toLong), + value = FuzzyValue.fromString + .applyOrElse(this.labelValue, (s: String) => throw new NoSuchElementException(s"Unknown fuzzy value $s")), + evidenceText = this.evidenceText, + document = this.document.toDomain + ) + +} object ApiExportPatientLabelEvidence { diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/export/ApiExportPatientLabelEvidenceDocument.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/export/ApiExportPatientLabelEvidenceDocument.scala index 99bb2cf..6999301 100644 --- a/src/main/scala/xyz/driver/pdsuidomain/formats/json/export/ApiExportPatientLabelEvidenceDocument.scala +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/export/ApiExportPatientLabelEvidenceDocument.scala @@ -1,16 +1,29 @@ package xyz.driver.pdsuidomain.formats.json.export import java.time.LocalDate +import java.util.UUID import play.api.libs.functional.syntax._ import play.api.libs.json.{Format, JsPath} +import xyz.driver.pdsuicommon.domain.LongId +import xyz.driver.pdsuidomain.entities.RecordRequestId import xyz.driver.pdsuidomain.entities.export.patient.ExportPatientLabelEvidenceDocument final case class ApiExportPatientLabelEvidenceDocument(documentId: String, requestId: String, documentType: String, providerType: String, - date: LocalDate) + date: LocalDate) { + + def toDomain = ExportPatientLabelEvidenceDocument( + documentId = LongId(this.documentId.toLong), + requestId = RecordRequestId(UUID.fromString(this.requestId)), + documentType = this.documentType, + providerType = this.providerType, + date = this.date + ) + +} object ApiExportPatientLabelEvidenceDocument { diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/export/ApiExportPatientWithLabels.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/export/ApiExportPatientWithLabels.scala index 8ce970b..fc9bab7 100644 --- a/src/main/scala/xyz/driver/pdsuidomain/formats/json/export/ApiExportPatientWithLabels.scala +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/export/ApiExportPatientWithLabels.scala @@ -2,9 +2,18 @@ package xyz.driver.pdsuidomain.formats.json.export import play.api.libs.functional.syntax._ import play.api.libs.json.{Format, JsPath} +import xyz.driver.pdsuicommon.domain.UuidId import xyz.driver.pdsuidomain.entities.export.patient.ExportPatientWithLabels -final case class ApiExportPatientWithLabels(patientId: String, labelVersion: Long, labels: List[ApiExportPatientLabel]) +final case class ApiExportPatientWithLabels(patientId: String, labelVersion: Long, labels: List[ApiExportPatientLabel]) { + + def toDomain = ExportPatientWithLabels( + patientId = UuidId(this.patientId), + labelVersion = this.labelVersion, + labels = this.labels.map(_.toDomain) + ) + +} object ApiExportPatientWithLabels { diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/extracteddata/ApiExtractedData.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/extracteddata/ApiExtractedData.scala index ec4185f..4182c8d 100644 --- a/src/main/scala/xyz/driver/pdsuidomain/formats/json/extracteddata/ApiExtractedData.scala +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/extracteddata/ApiExtractedData.scala @@ -1,5 +1,7 @@ package xyz.driver.pdsuidomain.formats.json.extracteddata +import xyz.driver.pdsuicommon.domain.{LongId, TextJson} +import xyz.driver.pdsuidomain.entities.ExtractedData import xyz.driver.pdsuidomain.formats.json.label.ApiExtractedDataLabel import play.api.libs.json._ import play.api.data.validation._ @@ -16,7 +18,20 @@ final case class ApiExtractedData(id: Long, evidence: Option[String], meta: Option[String], // An empty list and no-existent list are different cases - labels: Option[List[ApiExtractedDataLabel]]) + labels: Option[List[ApiExtractedDataLabel]]) { + + def toDomain = RichExtractedData( + extractedData = ExtractedData( + id = LongId(this.id), + documentId = LongId(this.documentId), + keywordId = this.keywordId.map(LongId(_)), + evidenceText = this.evidence, + meta = this.meta.map(x => TextJson(JsonSerializer.deserialize[ExtractedData.Meta](x))) + ), + labels = labels.getOrElse(List.empty).map(_.toDomain()) + ) + +} object ApiExtractedData { diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/record/ApiCreateRecord.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/record/ApiCreateRecord.scala index e96bc81..2e5943e 100644 --- a/src/main/scala/xyz/driver/pdsuidomain/formats/json/record/ApiCreateRecord.scala +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/record/ApiCreateRecord.scala @@ -7,7 +7,7 @@ import xyz.driver.pdsuicommon.domain._ import xyz.driver.pdsuidomain.entities._ import play.api.libs.json._ -final case class ApiCreateRecord(disease: String, patientId: String, requestId: UUID, filename: String) { +final case class ApiCreateRecord(disease: String, patientId: String, requestId: UUID) { def toDomain = MedicalRecord( id = LongId(0), @@ -21,7 +21,6 @@ final case class ApiCreateRecord(disease: String, patientId: String, requestId: disease = disease, caseId = None, physician = None, - sourceName = filename, meta = None, predictedMeta = None, predictedDocuments = None, diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/record/ApiRecord.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/record/ApiRecord.scala index 0e0b4d9..b255892 100644 --- a/src/main/scala/xyz/driver/pdsuidomain/formats/json/record/ApiRecord.scala +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/record/ApiRecord.scala @@ -1,12 +1,17 @@ package xyz.driver.pdsuidomain.formats.json.record import java.time.{ZoneId, ZonedDateTime} +import java.util.UUID +import xyz.driver.pdsuidomain.entities.CaseId import xyz.driver.pdsuidomain.entities.MedicalRecord +import xyz.driver.pdsuidomain.entities.MedicalRecord.Status +import xyz.driver.pdsuidomain.entities.RecordRequestId import play.api.data.validation.ValidationError import play.api.libs.functional.syntax._ import play.api.libs.json._ import xyz.driver.pdsuicommon.json.JsonSerializer +import xyz.driver.pdsuicommon.domain.{LongId, StringId, TextJson, UuidId} final case class ApiRecord(id: Long, patientId: String, @@ -19,7 +24,35 @@ final case class ApiRecord(id: Long, assignee: Option[String], previousAssignee: Option[String], lastActiveUser: Option[String], - meta: String) + requestId: UUID, + meta: String) { + + private def extractStatus(status: String): Status = + Status + .fromString(status) + .getOrElse( + throw new NoSuchElementException(s"Status $status not found") + ) + + def toDomain = MedicalRecord( + id = LongId(this.id), + status = extractStatus(this.status), + previousStatus = this.previousStatus.map(extractStatus), + assignee = this.assignee.map(StringId(_)), + previousAssignee = this.previousAssignee.map(StringId(_)), + lastActiveUserId = this.lastActiveUser.map(StringId(_)), + patientId = UuidId(patientId), + requestId = RecordRequestId(this.requestId), + disease = this.disease, + caseId = caseId.map(CaseId(_)), + physician = this.physician, + meta = Some(TextJson(JsonSerializer.deserialize[List[MedicalRecord.Meta]](this.meta))), + predictedMeta = None, + predictedDocuments = None, + lastUpdate = this.lastUpdate.toLocalDateTime() + ) + +} object ApiRecord { @@ -43,6 +76,7 @@ object ApiRecord { (JsPath \ "assignee").formatNullable[String] and (JsPath \ "previousAssignee").formatNullable[String] and (JsPath \ "lastActiveUser").formatNullable[String] and + (JsPath \ "requestId").format[UUID] and (JsPath \ "meta").format(Format(Reads { x => JsSuccess(Json.stringify(x)) }, Writes[String](Json.parse))) @@ -60,6 +94,7 @@ object ApiRecord { assignee = record.assignee.map(_.id), previousAssignee = record.previousAssignee.map(_.id), lastActiveUser = record.lastActiveUserId.map(_.id), + requestId = record.requestId.id, meta = record.meta.map(x => JsonSerializer.serialize(x.content)).getOrElse("[]") ) } diff --git a/src/main/scala/xyz/driver/pdsuidomain/services/MedicalRecordService.scala b/src/main/scala/xyz/driver/pdsuidomain/services/MedicalRecordService.scala index 46e9156..7a0502b 100644 --- a/src/main/scala/xyz/driver/pdsuidomain/services/MedicalRecordService.scala +++ b/src/main/scala/xyz/driver/pdsuidomain/services/MedicalRecordService.scala @@ -1,10 +1,12 @@ package xyz.driver.pdsuidomain.services +import akka.NotUsed +import akka.stream.scaladsl.Source +import akka.util.ByteString import java.time.LocalDateTime - -import xyz.driver.pdsuicommon.auth.AuthenticatedRequestContext +import xyz.driver.pdsuicommon.auth.{AnonymousRequestContext, AuthenticatedRequestContext} import xyz.driver.pdsuicommon.db._ -import xyz.driver.pdsuicommon.domain.{LongId, UuidId} +import xyz.driver.pdsuicommon.domain.LongId import xyz.driver.pdsuicommon.error._ import xyz.driver.pdsuidomain.entities.MedicalRecord.PdfSource import xyz.driver.pdsuidomain.entities._ @@ -37,24 +39,6 @@ object MedicalRecordService { extends GetByIdReply with DomainError.AuthorizationError with DefaultAccessDeniedError } - sealed trait GetPdfSourceReply - object GetPdfSourceReply { - type Error = GetPdfSourceReply with DomainError - - final case class Entity(x: PdfSource.Channel) extends GetPdfSourceReply - - case object AuthorizationError - extends GetPdfSourceReply with DomainError.AuthorizationError with DefaultAccessDeniedError - - case object NotFoundError extends GetPdfSourceReply with DomainError.NotFoundError { - def userMessage: String = "Medical record PDF hasn't been found" - } - - case object RecordNotFoundError extends GetPdfSourceReply with DomainError.NotFoundError with DefaultNotFoundError - - final case class CommonError(userMessage: String) extends GetPdfSourceReply with DomainError - } - sealed trait GetListReply object GetListReply { final case class EntityList(xs: Seq[MedicalRecord], totalFound: Int, lastUpdate: Option[LocalDateTime]) @@ -99,18 +83,15 @@ trait MedicalRecordService { def getById(recordId: LongId[MedicalRecord])( implicit requestContext: AuthenticatedRequestContext): Future[GetByIdReply] - def getPatientRecords(patientId: UuidId[Patient])( - implicit requestContext: AuthenticatedRequestContext): Future[GetListReply] - def getPdfSource(recordId: LongId[MedicalRecord])( - implicit requestContext: AuthenticatedRequestContext): Future[GetPdfSourceReply] + implicit requestContext: AuthenticatedRequestContext): Future[Source[ByteString, NotUsed]] def getAll(filter: SearchFilterExpr = SearchFilterExpr.Empty, sorting: Option[Sorting] = None, pagination: Option[Pagination] = None)( implicit requestContext: AuthenticatedRequestContext): Future[GetListReply] - def create(draft: MedicalRecord): Future[CreateReply] + def create(draft: MedicalRecord)(implicit requestContext: AnonymousRequestContext): Future[CreateReply] def update(origRecord: MedicalRecord, draftRecord: MedicalRecord)( implicit requestContext: AuthenticatedRequestContext): Future[UpdateReply] diff --git a/src/main/scala/xyz/driver/pdsuidomain/services/rest/RestDocumentService.scala b/src/main/scala/xyz/driver/pdsuidomain/services/rest/RestDocumentService.scala new file mode 100644 index 0000000..68fdde1 --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/services/rest/RestDocumentService.scala @@ -0,0 +1,111 @@ +package xyz.driver.pdsuidomain.services.rest + +import scala.concurrent.{ExecutionContext, Future} + +import akka.http.scaladsl.marshalling.Marshal +import akka.http.scaladsl.model._ +import akka.stream.Materializer +import xyz.driver.core.rest.{Pagination => _, _} +import xyz.driver.pdsuicommon.auth._ +import xyz.driver.pdsuicommon.db._ +import xyz.driver.pdsuicommon.domain._ +import xyz.driver.pdsuidomain.entities._ +import xyz.driver.pdsuidomain.formats.json.ListResponse +import xyz.driver.pdsuidomain.formats.json.document.ApiDocument +import xyz.driver.pdsuidomain.services.DocumentService + +class RestDocumentService(transport: ServiceTransport, baseUri: Uri)(implicit protected val materializer: Materializer, + protected val exec: ExecutionContext) + extends DocumentService with RestHelper { + + import xyz.driver.pdsuicommon.serialization.PlayJsonSupport._ + import xyz.driver.pdsuidomain.services.DocumentService._ + + def getById(id: LongId[Document])(implicit requestContext: AuthenticatedRequestContext): Future[GetByIdReply] = { + val request = HttpRequest(HttpMethods.GET, endpointUri(baseUri, s"/v1/document/$id")) + for { + response <- transport.sendRequestGetResponse(requestContext)(request) + reply <- apiResponse[ApiDocument](response) + } yield { + GetByIdReply.Entity(reply.toDomain) + } + } + + def getAll(filter: SearchFilterExpr = SearchFilterExpr.Empty, + sorting: Option[Sorting] = None, + pagination: Option[Pagination] = None)( + implicit requestContext: AuthenticatedRequestContext): Future[GetListReply] = { + + val request = HttpRequest(HttpMethods.GET, + endpointUri(baseUri, + "/v1/document", + filterQuery(filter) ++ sortingQuery(sorting) ++ paginationQuery(pagination))) + for { + response <- transport.sendRequestGetResponse(requestContext)(request) + reply <- apiResponse[ListResponse[ApiDocument]](response) + } yield { + GetListReply.EntityList(reply.items.map(_.toDomain), reply.meta.itemsCount, reply.meta.lastUpdate) + } + } + + def create(draftDocument: Document)(implicit requestContext: AuthenticatedRequestContext): Future[CreateReply] = { + for { + entity <- Marshal(ApiDocument.fromDomain(draftDocument)).to[RequestEntity] + request = HttpRequest(HttpMethods.POST, endpointUri(baseUri, "/v1/document")).withEntity(entity) + response <- transport.sendRequestGetResponse(requestContext)(request) + reply <- apiResponse[ApiDocument](response) + } yield { + CreateReply.Created(reply.toDomain) + } + } + + def update(orig: Document, draft: Document)( + implicit requestContext: AuthenticatedRequestContext): Future[UpdateReply] = { + for { + entity <- Marshal(ApiDocument.fromDomain(draft)).to[RequestEntity] + request = HttpRequest(HttpMethods.PATCH, endpointUri(baseUri, s"/v1/document/${orig.id}")).withEntity(entity) + response <- transport.sendRequestGetResponse(requestContext)(request) + reply <- apiResponse[ApiDocument](response) + } yield { + UpdateReply.Updated(reply.toDomain) + } + } + + def delete(id: LongId[Document])(implicit requestContext: AuthenticatedRequestContext): Future[DeleteReply] = { + val request = HttpRequest(HttpMethods.DELETE, endpointUri(baseUri, s"/v1/document/$id")) + for { + response <- transport.sendRequestGetResponse(requestContext)(request) + _ <- apiResponse[HttpEntity](response) + } yield { + DeleteReply.Deleted + } + } + + private def editAction(orig: Document, action: String)( + implicit requestContext: AuthenticatedRequestContext): Future[UpdateReply] = { + val id = orig.id.toString + val request = HttpRequest(HttpMethods.POST, endpointUri(baseUri, s"/v1/document/$id/$action")) + for { + response <- transport.sendRequestGetResponse(requestContext)(request) + reply <- apiResponse[ApiDocument](response) + } yield { + UpdateReply.Updated(reply.toDomain) + } + } + + def start(orig: Document)(implicit requestContext: AuthenticatedRequestContext): Future[UpdateReply] = + editAction(orig, "start") + def submit(orig: Document)(implicit requestContext: AuthenticatedRequestContext): Future[UpdateReply] = + editAction(orig, "submit") + def restart(orig: Document)(implicit requestContext: AuthenticatedRequestContext): Future[UpdateReply] = + editAction(orig, "restart") + def flag(orig: Document)(implicit requestContext: AuthenticatedRequestContext): Future[UpdateReply] = + editAction(orig, "flag") + def resolve(orig: Document)(implicit requestContext: AuthenticatedRequestContext): Future[UpdateReply] = + editAction(orig, "resolve") + def unassign(orig: Document)(implicit requestContext: AuthenticatedRequestContext): Future[UpdateReply] = + editAction(orig, "unassign") + def archive(orig: Document)(implicit requestContext: AuthenticatedRequestContext): Future[UpdateReply] = + editAction(orig, "archive") + +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/services/rest/RestDocumentTypeService.scala b/src/main/scala/xyz/driver/pdsuidomain/services/rest/RestDocumentTypeService.scala new file mode 100644 index 0000000..caa0042 --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/services/rest/RestDocumentTypeService.scala @@ -0,0 +1,32 @@ +package xyz.driver.pdsuidomain.services.rest + +import scala.concurrent.{ExecutionContext, Future} + +import akka.http.scaladsl.model._ +import akka.stream.Materializer +import xyz.driver.core.rest.{Pagination => _, _} +import xyz.driver.pdsuicommon.auth._ +import xyz.driver.pdsuicommon.db._ +import xyz.driver.pdsuidomain.formats.json.ListResponse +import xyz.driver.pdsuidomain.services.DocumentTypeService +import xyz.driver.pdsuidomain.formats.json.document.ApiDocumentType + +class RestDocumentTypeService(transport: ServiceTransport, baseUri: Uri)( + implicit protected val materializer: Materializer, + protected val exec: ExecutionContext) + extends DocumentTypeService with RestHelper { + + import xyz.driver.pdsuicommon.serialization.PlayJsonSupport._ + import xyz.driver.pdsuidomain.services.DocumentTypeService._ + + def getAll(sorting: Option[Sorting] = None)( + implicit requestContext: AuthenticatedRequestContext): Future[GetListReply] = { + val request = HttpRequest(HttpMethods.GET, endpointUri(baseUri, "/v1/document-type", sortingQuery(sorting))) + for { + response <- transport.sendRequestGetResponse(requestContext)(request) + reply <- apiResponse[ListResponse[ApiDocumentType]](response) + } yield { + GetListReply.EntityList(reply.items.map(_.toDomain), reply.meta.itemsCount) + } + } +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/services/rest/RestExtractedDataService.scala b/src/main/scala/xyz/driver/pdsuidomain/services/rest/RestExtractedDataService.scala new file mode 100644 index 0000000..53b9f38 --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/services/rest/RestExtractedDataService.scala @@ -0,0 +1,97 @@ +package xyz.driver.pdsuidomain.services.rest + +import scala.concurrent.{ExecutionContext, Future} + +import akka.http.scaladsl.marshalling.Marshal +import akka.http.scaladsl.model._ +import akka.stream.Materializer +import xyz.driver.core.rest.{Pagination => _, _} +import xyz.driver.pdsuicommon.auth._ +import xyz.driver.pdsuicommon.db._ +import xyz.driver.pdsuicommon.domain._ +import xyz.driver.pdsuidomain.entities._ +import xyz.driver.pdsuidomain.formats.json.ListResponse +import xyz.driver.pdsuidomain.formats.json.export.ApiExportPatientWithLabels +import xyz.driver.pdsuidomain.formats.json.extracteddata.ApiExtractedData +import xyz.driver.pdsuidomain.services.ExtractedDataService + +class RestExtractedDataService(transport: ServiceTransport, baseUri: Uri)( + implicit protected val materializer: Materializer, + protected val exec: ExecutionContext) + extends ExtractedDataService with RestHelper { + + import xyz.driver.pdsuicommon.serialization.PlayJsonSupport._ + import xyz.driver.pdsuidomain.services.ExtractedDataService._ + + def getById(id: LongId[ExtractedData])(implicit requestContext: AuthenticatedRequestContext): Future[GetByIdReply] = { + val request = HttpRequest(HttpMethods.GET, endpointUri(baseUri, s"/v1/extracted-data/$id")) + for { + response <- transport.sendRequestGetResponse(requestContext)(request) + reply <- apiResponse[ApiExtractedData](response) + } yield { + GetByIdReply.Entity(reply.toDomain) + } + } + + def getAll(filter: SearchFilterExpr = SearchFilterExpr.Empty, + sorting: Option[Sorting] = None, + pagination: Option[Pagination] = None)( + implicit requestContext: AuthenticatedRequestContext): Future[GetListReply] = { + val request = HttpRequest(HttpMethods.GET, + endpointUri(baseUri, + "/v1/extracted-data", + filterQuery(filter) ++ sortingQuery(sorting) ++ paginationQuery(pagination))) + for { + response <- transport.sendRequestGetResponse(requestContext)(request) + reply <- apiResponse[ListResponse[ApiExtractedData]](response) + } yield { + GetListReply.EntityList(reply.items.map(_.toDomain), reply.meta.itemsCount) + } + } + + def create(draftRichExtractedData: RichExtractedData)( + implicit requestContext: AuthenticatedRequestContext): Future[CreateReply] = { + for { + entity <- Marshal(ApiExtractedData.fromDomain(draftRichExtractedData)).to[RequestEntity] + request = HttpRequest(HttpMethods.POST, endpointUri(baseUri, "/v1/extracted-data")).withEntity(entity) + response <- transport.sendRequestGetResponse(requestContext)(request) + reply <- apiResponse[ApiExtractedData](response) + } yield { + CreateReply.Created(reply.toDomain) + } + } + def update(origRichExtractedData: RichExtractedData, draftRichExtractedData: RichExtractedData)( + implicit requestContext: AuthenticatedRequestContext): Future[UpdateReply] = { + val id = origRichExtractedData.extractedData.id + for { + entity <- Marshal(ApiExtractedData.fromDomain(draftRichExtractedData)).to[RequestEntity] + request = HttpRequest(HttpMethods.PATCH, endpointUri(baseUri, s"/v1/extracted-data/$id")).withEntity(entity) + response <- transport.sendRequestGetResponse(requestContext)(request) + reply <- apiResponse[ApiExtractedData](response) + } yield { + UpdateReply.Updated(reply.toDomain) + } + } + + def delete(id: LongId[ExtractedData])(implicit requestContext: AuthenticatedRequestContext): Future[DeleteReply] = { + val request = HttpRequest(HttpMethods.DELETE, endpointUri(baseUri, s"/v1/export-data/$id")) + for { + response <- transport.sendRequestGetResponse(requestContext)(request) + _ <- apiResponse[HttpEntity](response) + } yield { + DeleteReply.Deleted + } + } + + def getPatientLabels(id: UuidId[Patient])( + implicit requestContext: AuthenticatedRequestContext): Future[GetPatientLabelsReply] = { + val request = HttpRequest(HttpMethods.GET, endpointUri(baseUri, s"/v1/export/patient/$id")) + for { + response <- transport.sendRequestGetResponse(requestContext)(request) + reply <- apiResponse[ApiExportPatientWithLabels](response) + } yield { + GetPatientLabelsReply.Entity(reply.toDomain) + } + } + +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/services/rest/RestMedicalRecordService.scala b/src/main/scala/xyz/driver/pdsuidomain/services/rest/RestMedicalRecordService.scala new file mode 100644 index 0000000..f2b0f2d --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/services/rest/RestMedicalRecordService.scala @@ -0,0 +1,119 @@ +package xyz.driver.pdsuidomain.services.rest + +import akka.NotUsed +import akka.stream.scaladsl.Source +import akka.util.ByteString +import scala.concurrent.{ExecutionContext, Future} + +import akka.http.scaladsl.marshalling.Marshal +import akka.http.scaladsl.model._ +import akka.stream.Materializer +import xyz.driver.core.rest.{Pagination => _, _} +import xyz.driver.pdsuicommon.auth._ +import xyz.driver.pdsuicommon.db._ +import xyz.driver.pdsuicommon.domain._ +import xyz.driver.pdsuidomain.entities._ +import xyz.driver.pdsuidomain.formats.json.ListResponse +import xyz.driver.pdsuidomain.formats.json.record.ApiRecord +import xyz.driver.pdsuidomain.services.MedicalRecordService + +class RestMedicalRecordService(transport: ServiceTransport, baseUri: Uri)( + implicit protected val materializer: Materializer, + protected val exec: ExecutionContext) + extends MedicalRecordService with RestHelper { + + import xyz.driver.pdsuicommon.serialization.PlayJsonSupport._ + import xyz.driver.pdsuidomain.services.MedicalRecordService._ + + def getById(recordId: LongId[MedicalRecord])( + implicit requestContext: AuthenticatedRequestContext): Future[GetByIdReply] = { + val request = HttpRequest(HttpMethods.GET, endpointUri(baseUri, s"/v1/record/$recordId")) + for { + response <- transport.sendRequestGetResponse(requestContext)(request) + reply <- apiResponse[ApiRecord](response) + } yield { + GetByIdReply.Entity(reply.toDomain) + } + } + + def getPdfSource(recordId: LongId[MedicalRecord])( + implicit requestContext: AuthenticatedRequestContext): Future[Source[ByteString, NotUsed]] = { + + val request = HttpRequest(HttpMethods.GET, endpointUri(baseUri, s"/v1/record/${recordId}/source")) + + for { + response <- transport.sendRequestGetResponse(requestContext)(request) + reply <- apiResponse[HttpEntity](response) + } yield { + reply.dataBytes.mapMaterializedValue(_ => NotUsed) + } + } + + def getAll(filter: SearchFilterExpr = SearchFilterExpr.Empty, + sorting: Option[Sorting] = None, + pagination: Option[Pagination] = None)( + implicit requestContext: AuthenticatedRequestContext): Future[GetListReply] = { + + val request = HttpRequest( + HttpMethods.GET, + endpointUri(baseUri, "/v1/record", filterQuery(filter) ++ sortingQuery(sorting) ++ paginationQuery(pagination))) + for { + response <- transport.sendRequestGetResponse(requestContext)(request) + reply <- apiResponse[ListResponse[ApiRecord]](response) + } yield { + GetListReply.EntityList(reply.items.map(_.toDomain), reply.meta.itemsCount, reply.meta.lastUpdate) + } + } + + def create(draftRecord: MedicalRecord)(implicit requestContext: AnonymousRequestContext): Future[CreateReply] = { + for { + entity <- Marshal(ApiRecord.fromDomain(draftRecord)).to[RequestEntity] + request = HttpRequest(HttpMethods.POST, endpointUri(baseUri, "/v1/record")).withEntity(entity) + response <- transport.sendRequestGetResponse(requestContext)(request) + reply <- apiResponse[ApiRecord](response) + } yield { + CreateReply.Created(reply.toDomain) + } + } + + def update(origRecord: MedicalRecord, draftRecord: MedicalRecord)( + implicit requestContext: AuthenticatedRequestContext): Future[UpdateReply] = { + val id = origRecord.id.toString + for { + entity <- Marshal(ApiRecord.fromDomain(draftRecord)).to[RequestEntity] + request = HttpRequest(HttpMethods.PATCH, endpointUri(baseUri, s"/v1/record/$id")).withEntity(entity) + response <- transport.sendRequestGetResponse(requestContext)(request) + reply <- apiResponse[ApiRecord](response) + } yield { + UpdateReply.Updated(reply.toDomain) + } + } + + private def editAction(orig: MedicalRecord, action: String)( + implicit requestContext: AuthenticatedRequestContext): Future[UpdateReply] = { + val id = orig.id.toString + val request = HttpRequest(HttpMethods.POST, endpointUri(baseUri, s"/v1/record/$id/$action")) + for { + response <- transport.sendRequestGetResponse(requestContext)(request) + reply <- apiResponse[ApiRecord](response) + } yield { + UpdateReply.Updated(reply.toDomain) + } + } + + def start(orig: MedicalRecord)(implicit requestContext: AuthenticatedRequestContext): Future[UpdateReply] = + editAction(orig, "start") + def submit(orig: MedicalRecord)(implicit requestContext: AuthenticatedRequestContext): Future[UpdateReply] = + editAction(orig, "submit") + def restart(orig: MedicalRecord)(implicit requestContext: AuthenticatedRequestContext): Future[UpdateReply] = + editAction(orig, "restart") + def flag(orig: MedicalRecord)(implicit requestContext: AuthenticatedRequestContext): Future[UpdateReply] = + editAction(orig, "flag") + def resolve(orig: MedicalRecord)(implicit requestContext: AuthenticatedRequestContext): Future[UpdateReply] = + editAction(orig, "resolve") + def unassign(orig: MedicalRecord)(implicit requestContext: AuthenticatedRequestContext): Future[UpdateReply] = + editAction(orig, "unassign") + def archive(orig: MedicalRecord)(implicit requestContext: AuthenticatedRequestContext): Future[UpdateReply] = + editAction(orig, "archive") + +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/services/rest/RestMessageService.scala b/src/main/scala/xyz/driver/pdsuidomain/services/rest/RestMessageService.scala index e9c09c5..e133825 100644 --- a/src/main/scala/xyz/driver/pdsuidomain/services/rest/RestMessageService.scala +++ b/src/main/scala/xyz/driver/pdsuidomain/services/rest/RestMessageService.scala @@ -78,7 +78,7 @@ class RestMessageService(transport: ServiceTransport, baseUri: Uri)(implicit pro val request = HttpRequest(HttpMethods.DELETE, endpointUri(baseUri, s"/v1/message/${messageId.id}")) for { response <- transport.sendRequestGetResponse(requestContext)(request) - reply <- apiResponse[ApiMessage](response) + _ <- apiResponse[HttpEntity](response) } yield { DeleteReply.Deleted } diff --git a/src/main/scala/xyz/driver/pdsuidomain/services/rest/RestProviderTypeService.scala b/src/main/scala/xyz/driver/pdsuidomain/services/rest/RestProviderTypeService.scala new file mode 100644 index 0000000..f82ec40 --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/services/rest/RestProviderTypeService.scala @@ -0,0 +1,33 @@ +package xyz.driver.pdsuidomain.services.rest + +import scala.concurrent.{ExecutionContext, Future} + +import akka.http.scaladsl.model._ +import akka.stream.Materializer +import xyz.driver.core.rest.{Pagination => _, _} +import xyz.driver.pdsuicommon.auth._ +import xyz.driver.pdsuicommon.db._ +import xyz.driver.pdsuidomain.formats.json.ListResponse +import xyz.driver.pdsuidomain.services.ProviderTypeService +import xyz.driver.pdsuidomain.formats.json.document.ApiProviderType + +class RestProviderTypeService(transport: ServiceTransport, baseUri: Uri)( + implicit protected val materializer: Materializer, + protected val exec: ExecutionContext) + extends ProviderTypeService with RestHelper { + + import xyz.driver.pdsuicommon.serialization.PlayJsonSupport._ + import xyz.driver.pdsuidomain.services.ProviderTypeService._ + + def getAll(sorting: Option[Sorting] = None)( + implicit requestContext: AuthenticatedRequestContext): Future[GetListReply] = { + val request = HttpRequest(HttpMethods.GET, endpointUri(baseUri, "/v1/provider-type", sortingQuery(sorting))) + for { + response <- transport.sendRequestGetResponse(requestContext)(request) + reply <- apiResponse[ListResponse[ApiProviderType]](response) + } yield { + GetListReply.EntityList(reply.items.map(_.toDomain), reply.meta.itemsCount) + } + } + +} -- cgit v1.2.3