All Downloads are FREE. Search and download functionalities are using the official Maven repository.

a8.sync.dsl.scala Maven / Gradle / Ivy

The newest version!
package a8.sync


import java.sql.ResultSet
import a8.sync.dsl.JsonPath
import org.typelevel.ci.CIString
import Imports.*
import a8.common.logging.Level
import a8.sync.ResolvedTable.{ColumnMapper, DataType, ResolvedField}
import a8.sync.impl.{NormalizedKey, NormalizedRow, NormalizedTuple, NormalizedValue, SqlValue, resolveMapping}
import a8.shared.jdbcf.{ColumnName, Conn, Dialect, SchemaName, SqlString, TableName}
import a8.shared.json.DynamicJson
import a8.shared.json.ast.{JsNothing, JsVal}
import a8.sync.RowSync.ValidationMessage
import cats.data.Chain

import zio.*

/**

 TODO actually take a sync and apply it to a database
 TODO we likely need a way to generate primary keys
 TODO we likely need a way to support side car tables

 DONE query the database to determine the primary keys
 DONE do some JNumber mangling to the same type(s) probly JDecimal so that keys and equals "just works (tm)"

 */
object dsl {

  case class Mapping(
    tables: Vector[Table],
    postDocumentAcquisitionFilter: DynamicJson=>Boolean = _ => true,
    defaultTruncationAction: TruncateAction = TruncateAction.Error,
  ) {
//    def resolveToSchema(schema: String)(implicit dialect: Dialect): ConnectionIO[ResolvedMapping] =
//      resolveMapping(schema, this)
  }

  sealed trait MappingResult

  object MappingResult {
    case class Success(document: DynamicJson, rowSyncs: Chain[RowSync], validationMessages: Chain[ValidationMessage]) extends MappingResult
    case class Error(document: DynamicJson, throwable: Throwable, validationMessages: Chain[ValidationMessage] = Chain.empty) extends MappingResult
    case class Skip(document: DynamicJson, validationMessages: Chain[ValidationMessage] = Chain.empty) extends MappingResult
  }

  case class MappingException(document: DynamicJson, throwable: Throwable) extends Exception(throwable)

  case class SkipMappingException(document: DynamicJson, reason: String) extends Exception(reason)

  case class ResolvedMapping(schema: SchemaName, tables: Vector[ResolvedTable], mapping: Mapping) {

    def processRootDocument(document: DynamicJson, conn: Conn): Task[MappingResult] = {
      mapping
        .postDocumentAcquisitionFilter(document)
        .toOption(
          tables
            .map(table => table.runSync(document, conn, mapping.defaultTruncationAction))
            .sequence
            .map(_.foldLeft(Chain.empty[RowSync])(_ ++ _))
            .map { rowSyncs =>
              val validationMessages = rowSyncs.flatMap(_.validationMessages)
              validationMessages.find(_._1 >= Level.Error) match {
                case Some(firstError) =>
                  MappingResult.Error(document, new RuntimeException(s"Validation error found, the first error is -- ${firstError._2}"), validationMessages) : MappingResult
                case None =>
                  MappingResult.Success(document, rowSyncs, validationMessages) : MappingResult
              }
            }
            .catchAll(th => ZIO.succeed(MappingResult.Error(document, th, Chain.empty)))
        )
        .getOrElse(ZIO.succeed(MappingResult.Skip(document, Chain(Level.Info -> "skipped claim -- postProcessDocumentFilter returned false"))))
    }

  }

  case class Table(
    sourceRows: JsonPath,
    sourceRowsFilter: DynamicJson => Boolean = _ => true,
    targetTable: TableName,
    syncWhereClause: DynamicJson => SqlString,
    fields: Iterable[Field],
  ) {
    lazy val fieldsAsChunk: Chunk[Field] = Chunk.fromArray(fields.toArray)

    def addFields(moreFields: Iterable[Field]): Table =
      copy(fields = fields ++ moreFields)

  }

//  case class ColumnName(value: String) {
//    val unquoted = value.stripQuotes
//    lazy val ci = CIString(unquoted)
//  }

  sealed abstract class TruncateAction(val logLevel: Option[Level]) extends enumeratum.EnumEntry
  object TruncateAction extends enumeratum.Enum[TruncateAction] {
    val values = findValues

    case object Error extends TruncateAction(Some(Level.Error))
    case object Warn extends TruncateAction(Some(Level.Warn))
    case object Auto extends TruncateAction(None)

  }

  case class Field(
    from: JsonPath,
    toColumnName: ColumnName,
    keyOrdinal: Option[Int] = None,
//    nullable: Boolean = false,
    defaultValue: JsVal = JsNothing,
    dataType: DataType = DataType.ColumnDefault,
    truncateAction: Option[TruncateAction] = None,
    omitOnInsertUpdate: DynamicJson => Boolean = _ => false,
  ) {
    def dataType(dataType: DataType): Field =
      copy(dataType = dataType)
    def primaryKey(keyOrdinal: Int): Field =
      copy(keyOrdinal = Some(keyOrdinal))
    def defaultValue(defaultValue: JsVal): Field =
      copy(defaultValue = defaultValue)
    def truncateAction(truncateAction: TruncateAction): Field =
      copy(truncateAction = Some(truncateAction))
    def omitOnInsertUpdate(omitOnInsertUpdate: DynamicJson => Boolean = _ => true): Field =
      copy(omitOnInsertUpdate = omitOnInsertUpdate)
  }

  case class JsonPath(
    getter: DynamicJson => DynamicJson
  )

  object JsonPath {

  }




}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy