-
-
Save yujikiriki/9996830 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 scala.concurrent.Future | |
import play.api.Logger | |
import reactivemongo.core.commands.LastError | |
import reactivemongo.core.errors.DatabaseException | |
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 10148 => { | |
Left(OperationNotAllowedException("", nestedException = e)) | |
} | |
case 11000 => { | |
Left(DuplicateResourceException(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.helpers | |
import scala.concurrent.ExecutionContext | |
/** | |
* Helper around implicit contexts. | |
* | |
* @author Pedro De Almeida (almeidap) | |
*/ | |
trait ContextHelper { | |
implicit def ec: ExecutionContext = ExecutionContext.Implicits.global | |
} |
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[T](field: String, data: T)(implicit writer: Writes[T]): JsObject = set(Json.obj(field -> data)) | |
def set(data: JsObject): JsObject = Json.obj("$set" -> 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)) | |
def inc(field: String, amount: Int) = Json.obj("$inc" -> Json.obj(field -> amount)) | |
def or(criterias: JsObject*): JsObject = Json.obj("$or" -> criterias) | |
def gt[T](field: String, value: T)(implicit writer: Writes[T]) = Json.obj(field -> Json.obj("$gt" -> value)) | |
def lt[T](field: String, value: T)(implicit writer: Writes[T]) = Json.obj(field -> Json.obj("$lt" -> value)) | |
def query[T](query: T)(implicit writer: Writes[T]): JsObject = Json.obj("$query" -> query) | |
def orderBy[T](query: T)(implicit writer: Writes[T]): JsObject = Json.obj("$orderby" -> query) | |
} |
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 org.joda.time.DateTime | |
import scala.concurrent.Future | |
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 | |
/** | |
* 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]") | |
collection.find(query).cursor[T].collect[List]() | |
} | |
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, query: JsObject): Future[Either[ServiceException, JsObject]] = { | |
val data = updated(query) | |
Logger.debug(s"Updating by query: [collection=$collectionName, id=$id, query=$data]") | |
Recover(collection.update(DBQueryBuilder.id(id), data)) { | |
data | |
} | |
} | |
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(data: JsObject) = { | |
data.validate((__ \ '$set).json.update( | |
__.read[JsObject].map{ o => o ++ Json.obj("updated" -> DateTime.now) } | |
)).fold( | |
error => data, | |
success => success | |
) | |
} | |
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.dao | |
import scala.concurrent.Future | |
import play.api.Logger | |
import play.api.libs.json.{Reads, Json, JsObject} | |
import reactivemongo.api.Cursor | |
import reactivemongo.api.gridfs.{ReadFile, GridFS} | |
import reactivemongo.bson.{BSONValue, BSONObjectID} | |
import core.db.DBQueryBuilder | |
import core.exceptions.ServiceException | |
/* Implicits */ | |
import play.modules.reactivemongo.json.ImplicitBSONHandlers._ | |
import reactivemongo.api.gridfs.Implicits.DefaultReadFileReader | |
/** | |
* DAO for MongoDB `GridFS` files. | |
* | |
* @author Pedro De Almeida (almeidap) | |
*/ | |
trait FileDAO extends BaseDAO { | |
lazy val gfs = GridFS(db, collectionName) | |
def find(query: JsObject = Json.obj()): Cursor[ReadFile[BSONValue]] = { | |
Logger.debug(s"Finding files: [collection=$collectionName, query=$query]") | |
gfs.find(query) | |
} | |
def findById(id: String): Future[Option[ReadFile[BSONValue]]] = find(DBQueryBuilder.id(id)).headOption | |
def findOne(query: JsObject = Json.obj()): Future[Option[ReadFile[BSONValue]]] = { | |
Logger.debug(s"Finding one file: [collection=$collectionName, query=$query]") | |
gfs.find(query).headOption | |
} | |
def removeById(id: String): Future[Either[ServiceException, Boolean]] = { | |
Recover(gfs.remove(BSONObjectID(id))) { | |
true | |
} | |
} | |
override def ensureIndexes = { | |
// Let's build an index on our gridfs chunks collection if none: | |
gfs.ensureIndex.map { | |
case status => | |
Logger.info(s"GridFS index: [collection=$collectionName, status=$status]") | |
List(status) | |
} | |
} | |
} |
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 core.helpers.ContextHelper | |
/** | |
* Helper around `MongoDB` resources. | |
* | |
* @author Pedro De Almeida (almeidap) | |
*/ | |
trait MongoHelper extends ContextHelper{ | |
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 | |
/** | |
* Trait for service exceptions. | |
* | |
* @author Pedro De Almeida (almeidap) | |
*/ | |
trait ServiceException extends Exception { | |
val message: String | |
val nestedException: Throwable | |
} |
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 | |
import core.exceptions.ServiceException | |
/** | |
* Service-related exceptions. | |
* | |
* @author Pedro De Almeida (almeidap) | |
*/ | |
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