-
-
Save anatolyg/6042119 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package core.dao | |
import play.api.Logger | |
import reactivemongo.core.commands.LastError | |
import reactivemongo.core.errors.DatabaseException | |
import scala.concurrent.Future | |
import core.db.MongoHelper | |
import core.exceptions._ | |
/* Implicits */ | |
import play.modules.reactivemongo.json.BSONFormats._ | |
/** | |
* Base DAO for MongoDB resources. | |
* | |
* @author Pedro De Almeida (almeidap) | |
*/ | |
trait BaseDAO extends MongoHelper { | |
val collectionName: String | |
def ensureIndexes: Future[List[Boolean]] | |
def Recover[S](operation: Future[LastError])(success: => S): Future[Either[ServiceException, S]] = { | |
operation.map { | |
lastError => lastError.inError match { | |
case true => { | |
Logger.error(s"DB operation did not perform successfully: [lastError=$lastError]") | |
Left(DBServiceException(lastError)) | |
} | |
case false => { | |
Right(success) | |
} | |
} | |
} recover { | |
case exception => | |
Logger.error(s"DB operation failed: [message=${exception.getMessage}]") | |
// TODO: better failure handling here | |
val handling: Option[Either[ServiceException, S]] = exception match { | |
case e: DatabaseException => { | |
e.code.map(code => { | |
Logger.error(s"DatabaseException: [code=${code}, isNotAPrimaryError=${e.isNotAPrimaryError}]") | |
code match { | |
case 11000 => { | |
Left(DuplicateResourceException(nestedException = e)) | |
} | |
case 10148 => { | |
Left(OperationNotAllowedException("", nestedException = e)) | |
} | |
} | |
}) | |
} | |
} | |
handling.getOrElse(Left(UnexpectedServiceException(exception.getMessage, nestedException = exception))) | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package core.db | |
import play.api.libs.json.{Writes, Json, JsObject} | |
import reactivemongo.bson.BSONObjectID | |
/* Implicits */ | |
import play.modules.reactivemongo.json.BSONFormats._ | |
/** | |
* Query builder wrapping common queries and MongoDB operators. | |
* | |
* TODO: create a real query `builder` | |
* | |
* @author Pedro De Almeida (almeidap) | |
*/ | |
object DBQueryBuilder { | |
def id(objectId: String): JsObject = id(BSONObjectID(objectId)) | |
def id(objectId: BSONObjectID): JsObject = Json.obj("_id" -> objectId) | |
def set(field: String, data: JsObject): JsObject = set(Json.obj(field -> data)) | |
def set(data: JsObject): JsObject = Json.obj("$set" -> data) | |
def set[T](field: String, data: T)(implicit writer: Writes[T]): JsObject = set(Json.obj(field -> data)) | |
def set[T](data: T)(implicit writer: Writes[T]): JsObject = Json.obj("$set" -> data) | |
def push[T](field: String, data: T)(implicit writer: Writes[T]): JsObject = Json.obj("$push" -> Json.obj(field -> data)) | |
def pull[T](field: String, query: T)(implicit writer: Writes[T]): JsObject = Json.obj("$pull" -> Json.obj(field -> query)) | |
def unset(field: String): JsObject = Json.obj("$unset" -> Json.obj(field -> 1)) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package core.dao | |
import play.api.Logger | |
import play.api.libs.json._ | |
import play.modules.reactivemongo.json.collection.JSONCollection | |
import reactivemongo.api.indexes.{Index, IndexType} | |
import reactivemongo.bson._ | |
import core.db.DBQueryBuilder | |
import core.exceptions.ServiceException | |
import core.models.TemporalModel | |
import org.joda.time.DateTime | |
import scala.concurrent.Future | |
/** | |
* DAO for MongoDB documents. | |
* | |
* @author Pedro De Almeida (almeidap) | |
*/ | |
trait DocumentDAO[T <: TemporalModel] extends BaseDAO { | |
lazy val collection = db.collection[JSONCollection](collectionName) | |
def insert(document: T)(implicit writer: Writes[T]): Future[Either[ServiceException, T]] = { | |
document._id = Some(BSONObjectID.generate) | |
document.created = Some(DateTime.now) | |
document.updated = Some(DateTime.now) | |
Logger.debug(s"Inserting document: [collection=$collectionName, data=$document]") | |
Recover(collection.insert(document)) { | |
document | |
} | |
} | |
def find(query: JsObject = Json.obj())(implicit reader: Reads[T]): Future[List[T]] = { | |
Logger.debug(s"Finding documents: [collection=$collectionName, query=$query, c=$collection]") | |
collection.find(query).cursor[T].toList | |
} | |
def findById(id: String)(implicit reader: Reads[T]): Future[Option[T]] = findOne(DBQueryBuilder.id(id)) | |
def findById(id: BSONObjectID)(implicit reader: Reads[T]): Future[Option[T]] = findOne(DBQueryBuilder.id(id)) | |
def findOne(query: JsObject)(implicit reader: Reads[T]): Future[Option[T]] = { | |
Logger.debug(s"Finding one: [collection=$collectionName, query=$query]") | |
collection.find(query).one[T] | |
} | |
def update(id: String, document: T)(implicit writer: Writes[T]): Future[Either[ServiceException, T]] = { | |
document.updated = Some(new DateTime()) | |
Logger.debug(s"Updating document: [collection=$collectionName, id=$id, document=$document]") | |
Recover(collection.update(DBQueryBuilder.id(id), DBQueryBuilder.set(document))) { | |
document | |
} | |
} | |
def update(id: String, data: JsObject): Future[Either[ServiceException, JsObject]] = { | |
val update = data ++ updated | |
Logger.debug(s"Updating data: [collection=$collectionName, id=$id, update=$update]") | |
Recover(collection.update(DBQueryBuilder.id(id), DBQueryBuilder.set(update))) { | |
update | |
} | |
} | |
def push[S](id: String, field: String, data: S)(implicit writer: Writes[S]): Future[Either[ServiceException, S]] = { | |
Logger.debug(s"Pushing to document: [collection=$collectionName, id=$id, field=$field data=$data]") | |
Recover(collection.update(DBQueryBuilder.id(id), DBQueryBuilder.push(field, data) | |
)) { | |
data | |
} | |
} | |
def pull[S](id: String, field: String, query: S)(implicit writer: Writes[S]): Future[Either[ServiceException, Boolean]] = { | |
Logger.debug(s"Pulling from document: [collection=$collectionName, id=$id, field=$field query=$query]") | |
Recover(collection.update(DBQueryBuilder.id(id), DBQueryBuilder.pull(field, query))) { | |
true | |
} | |
} | |
def unset(id: String, field: String): Future[Either[ServiceException, Boolean]] = { | |
Logger.debug(s"Unsetting from document: [collection=$collectionName, id=$id, field=$field]") | |
Recover(collection.update(DBQueryBuilder.id(id), DBQueryBuilder.unset(field))) { | |
true | |
} | |
} | |
def remove(id: String): Future[Either[ServiceException, Boolean]] = remove(BSONObjectID(id)) | |
def remove(id: BSONObjectID): Future[Either[ServiceException, Boolean]] = { | |
Logger.debug(s"Removing document: [collection=$collectionName, id=$id]") | |
Recover( | |
collection.remove(DBQueryBuilder.id(id)) | |
) { | |
true | |
} | |
} | |
def remove(query: JsObject, firstMatchOnly: Boolean = false): Future[Either[ServiceException, Boolean]] = { | |
Logger.debug(s"Removing document(s): [collection=$collectionName, firstMatchOnly=$firstMatchOnly, query=$query]") | |
Recover( | |
collection.remove(query, firstMatchOnly = firstMatchOnly) | |
) { | |
true | |
} | |
} | |
def updated = Json.obj("updated" -> DateTime.now) | |
def ensureIndex( | |
key: List[(String, IndexType)], | |
name: Option[String] = None, | |
unique: Boolean = false, | |
background: Boolean = false, | |
dropDups: Boolean = false, | |
sparse: Boolean = false, | |
version: Option[Int] = None, | |
options: BSONDocument = BSONDocument()) = { | |
val index = Index(key, name, unique, background, dropDups, sparse, version, options) | |
Logger.info(s"Ensuring index: $index") | |
collection.indexesManager.ensure(index) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package core.models | |
import reactivemongo.bson.BSONObjectID | |
/** | |
* Base model for `identifiable` documents. | |
* | |
* @author Pedro De Almeida (almeidap) | |
*/ | |
trait IdentifiableModel { | |
var _id: Option[BSONObjectID] | |
def identify = _id.map(value => value.stringify).getOrElse("") | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package core.db | |
import play.api.Play.current | |
import play.modules.reactivemongo.ReactiveMongoPlugin | |
import reactivemongo.bson.{BSONObjectID, BSONValue} | |
import scala.concurrent.ExecutionContext | |
/** | |
* Helper around `MongoDB` resources. | |
* | |
* @author Pedro De Almeida (almeidap) | |
*/ | |
trait MongoHelper { | |
implicit def ec: ExecutionContext = ExecutionContext.Implicits.global | |
lazy val db = ReactiveMongoPlugin.db | |
} | |
object MongoHelper extends MongoHelper { | |
def identify(bson: BSONValue) = bson.asInstanceOf[BSONObjectID].stringify | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package core.exceptions | |
import reactivemongo.core.commands.LastError | |
/** | |
* Service-related exceptions. | |
* | |
* @author Pedro De Almeida (almeidap) | |
*/ | |
trait ServiceException extends Exception { | |
val message: String | |
val nestedException: Throwable | |
} | |
case class UnexpectedServiceException( | |
message: String, | |
nestedException: Throwable = null | |
) extends ServiceException | |
case class DBServiceException( | |
message: String, | |
lastError: Option[LastError] = None, | |
nestedException: Throwable = null | |
) extends ServiceException | |
object DBServiceException { | |
def apply(lastError: LastError): ServiceException = { | |
DBServiceException(lastError.errMsg.getOrElse(lastError.message), Some(lastError)) | |
} | |
} | |
case class DuplicateResourceException( | |
message: String = "error.duplicate.resource", | |
nestedException: Throwable = null | |
) extends ServiceException | |
case class OperationNotAllowedException( | |
message: String = "error.operation.not.allowed", | |
nestedException: Throwable = null | |
) extends ServiceException | |
case class ResourceNotFoundException( | |
id: String, | |
message: String = "error.resource.not.found", | |
nestedException: Throwable = null | |
) extends ServiceException |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package core.models | |
import org.joda.time.DateTime | |
/** | |
* Base model for `temporal` documents. | |
* | |
* @author Pedro De Almeida (almeidap) | |
*/ | |
trait TemporalModel extends IdentifiableModel { | |
var created: Option[DateTime] | |
var updated: Option[DateTime] | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment