Skip to content

LearnCore Technical Challenge #1

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
227 changes: 210 additions & 17 deletions app/controllers/SCIMController.scala
Original file line number Diff line number Diff line change
@@ -1,51 +1,244 @@
package controllers

import javax.inject._
import play.api.db.Database
import play.api.mvc._
import play.api.libs.json._

import javax.inject._
import java.sql.ResultSet

import com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException

class SCIMController @Inject() (db:Database) extends Controller {

def users(filter:Option[String], count:Option[String], startIndex:Option[String]): Result = {
// ----------- Users ----------- //

def users(filter:Option[String], count:Option[String], startIndex:Option[String]) = Action {
// TODO: Retrieve paginated User Objects
// TODO: Allow for an equals and startsWith filters on username
Ok
val query = "SELECT * FROM users" +
filter.map(" WHERE username LIKE '" + _ + "%'").getOrElse("") +
count.map(" LIMIT " + _).getOrElse("") +
startIndex.map(" OFFSET " + _).getOrElse("")

db.withConnection { conn =>
val stmt = conn.createStatement
val rs = stmt.executeQuery(query)
val users: Stream[JsValue] = results(rs)(parseUser)

Ok(Json.obj("users" -> users))
}
}

def user(uid:String): Result = {
def user(uid:String) = Action {
// TODO: Retrieve a single User Object by ID
Ok
val query = s"SELECT * FROM users WHERE id = '${uid}'"

db.withConnection { conn =>
val stmt = conn.createStatement
val rs = stmt.executeQuery(query)

if (rs.next())
Ok(parseUser(rs))
else
NotFound
}
}

def createUser(): Result = {
def createUser() = Action { implicit request: Request[AnyContent] =>
// TODO: Create a User Object with firstname and lastname metadata
Ok
val id = field[String]("id")
val username = field[String]("username")
val firstname = field[String]("firstname")
val lastname = field[String]("lastname")
val active = field[Boolean]("active", false)

try {
doCreateUser(id, username, firstname, lastname, active)
} catch {
case dup: MySQLIntegrityConstraintViolationException => BadRequest(dup.getMessage)
case _: Throwable => InternalServerError
}
}

def updateUser(uid:String): Result = {
def updateUser(uid:String) = Action { implicit request: Request[AnyContent] =>
// TODO: Update a User Object's firstname, lastname, and active status
Ok
val firstname = fieldOpt[String]("firstname")
val lastname = fieldOpt[String]("lastname")
val active = fieldOpt[Boolean]("active")

doUpdateUser(Some(uid), firstname, lastname, active)
}

def deleteUser(uid:String): Result = {
def deleteUser(uid:String) = Action {
// TODO: Delete a User Object by ID
Ok
doDeleteUser(uid)
}

def groups(count:Option[String], startIndex:Option[String]): Result = {
// ----------- Groups ----------- //

def groups(count:Option[String], startIndex:Option[String]) = Action {
// TODO: Retrieve paginated Group Objects
Ok
val query = "SELECT * FROM groups" +
count.map(" LIMIT " + _).getOrElse("") +
startIndex.map(" OFFSET " + _).getOrElse("")

db.withConnection { conn =>
val stmt = conn.createStatement
val rs = stmt.executeQuery(query)
val groups: Stream[JsValue] = results(rs)(parseGroup)

Ok(Json.obj("groups" -> groups))
}
}

def group(groupId:String): Result = {
def group(groupId:String) = Action {
// TODO: Retrieve a single Group Object by ID
Ok
val query = s"""
SELECT users.* FROM users
INNER JOIN users_groups
ON users.id = users_groups.user_id
WHERE group_id = '${groupId}'"""

db.withConnection { conn =>
val stmt = conn.createStatement
val rs = stmt.executeQuery(query)
val users: Stream[JsValue] = results(rs)(parseUser)

Ok(Json.obj(
"id" -> groupId,
"users" -> users))
}
}

def patchGroup(groupId:String): Result = {
def patchGroup(groupId:String) = Action { implicit request: Request[AnyContent] =>
// TODO: Patch a Group Object, modifying its members
Ok
val add = fieldOpt[List[JsValue]]("add")
val delete = fieldOpt[List[String]]("delete")
val update = fieldOpt[List[JsValue]]("update")

if (add.isEmpty && delete.isEmpty && update.isEmpty) {
BadRequest("empty patch request")
} else {
// this should probably be in a transaction to roll back in case of an exception
try {
add.map(_.foreach(user => (doCreateUser _).tupled(toUserTuple(user)))) // this ignores the result of calls to doCreateUser
delete.map(_.foreach(doDeleteUser)) // this ignores the result of calls to doDeleteUser
update.map(_.foreach(user => (doUpdateUser _).tupled(toUserOptTuple(user)))) // this ignores the result of calls to doUpdateUser
Ok
} catch {
case dup: MySQLIntegrityConstraintViolationException => BadRequest(dup.getMessage)
case _: Throwable => InternalServerError
}
}
}

// ----------- Utilities ----------- //

// taken from https://stackoverflow.com/questions/9636545/treating-an-sql-resultset-like-a-scala-stream
private def results[T](resultSet: ResultSet)(f: ResultSet => T): Stream[T] = {
new Iterator[T] {
def hasNext = resultSet.next()
def next() = f(resultSet)
}.toStream
}

private def parseUser(rs: ResultSet): JsValue =
Json.obj(
"id" -> rs.getString("id"),
"username" -> rs.getString("username"),
"firstname" -> rs.getString("firstname"),
"lastname" -> rs.getString("lastname"),
"active" -> (if (rs.getInt("active") == 1) "true" else "false")
)

private def parseGroup(rs: ResultSet): JsValue =
Json.obj(
"id" -> rs.getString("id")
)

private def toUserTuple(user: JsValue) =
(
(user \ "id").asOpt[String].getOrElse(null),
(user \ "username").asOpt[String].getOrElse(null),
(user \ "firstname").asOpt[String].getOrElse(null),
(user \ "lastname").asOpt[String].getOrElse(null),
(user \ "active").asOpt[Boolean].getOrElse(false))

private def toUserOptTuple(user: JsValue) =
(
(user \ "id").asOpt[String],
(user \ "firstname").asOpt[String],
(user \ "lastname").asOpt[String],
(user \ "active").asOpt[Boolean])

private def doCreateUser(id: String, username: String, firstname: String, lastname: String, active: Boolean) = {
if (id == null) {
BadRequest("missing id")
} else if (username == null) {
BadRequest("missing username")
} else if (firstname == null) {
BadRequest("missing firstname")
} else if (lastname == null) {
BadRequest("missing lastname")
} else {
val query = s"""
INSERT INTO users (id, username, firstname, lastname, active)
VALUES
('${id}', '${username}', '${firstname}', '${lastname}', ${active});
"""

db.withConnection { conn =>
val stmt = conn.createStatement
val rc = stmt.executeUpdate(query)

if (rc == 1)
Ok
else
InternalServerError("failed to create user")
}
}
}

private def doDeleteUser(id: String) = {
val query = s"DELETE FROM users WHERE id = '${id}'"

db.withConnection { conn =>
val stmt = conn.createStatement
val rc = stmt.executeUpdate(query)

if (rc == 1)
Ok
else
NotFound
}
}

private def doUpdateUser(id: Option[String], firstname: Option[String], lastname: Option[String], active: Option[Boolean]) = {
if (id.isEmpty) {
BadRequest("missing id")
} else if (firstname.isEmpty && lastname.isEmpty && active.isEmpty) {
BadRequest("must pass at least one of firstname, lastname, active to update")
} else {
val query = "UPDATE users SET" +
(firstname.map(" firstname = '" + _ + "',").getOrElse("") +
lastname.map(" lastname = '" + _ + "',").getOrElse("") +
active.map(" active = " + _ + ",").getOrElse("")).dropRight(1) + // dropRight removes extra trailing comma
s" WHERE id = '${id.get}'"

db.withConnection { conn =>
val stmt = conn.createStatement
val rc = stmt.executeUpdate(query)

if (rc == 1)
Ok("user updated")
else
NotFound
}
}
}

private def field[T: Reads](field: String, default: T = null)(implicit request: Request[AnyContent]) = fieldOpt[T](field).getOrElse(default)
private def fieldOpt[T: Reads](field: String)(implicit request: Request[AnyContent]) = request.body.asJson.map(_ \ field).flatMap(_.asOpt[T])

}
5 changes: 3 additions & 2 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ scalaVersion := "2.11.7"

libraryDependencies ++= Seq(
javaJdbc,
evolutions,
jdbc,
"com.typesafe.play" %% "anorm" % "2.5.0",
"mysql" % "mysql-connector-java" % "5.1.23",
cache,
Expand All @@ -16,5 +18,4 @@ libraryDependencies ++= Seq(
)



fork in run := true
fork in run := false
11 changes: 6 additions & 5 deletions conf/application.conf
Original file line number Diff line number Diff line change
Expand Up @@ -341,11 +341,12 @@ db {
# You can declare as many datasources as you want.
# By convention, the default datasource is named `default`

# https://www.playframework.com/documentation/latest/Developing-with-the-H2-Database
#default.driver = org.h2.Driver
#default.url = "jdbc:h2:mem:play"
#default.username = sa
#default.password = ""
default {
driver=com.mysql.jdbc.Driver
url="jdbc:mysql://localhost/scim"
username=scim
password="scim"
}

# You can turn on SQL logging for any datasource
# https://www.playframework.com/documentation/latest/Highlights25#Logging-SQL-statements
Expand Down
16 changes: 16 additions & 0 deletions conf/evolutions/default/1.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Users schema

# --- !Ups

CREATE TABLE users (
id varchar(36) NOT NULL,
username varchar(256) NOT NULL,
firstname varchar(256) NOT NULL,
lastname varchar(256) NOT NULL,
active tinyint(1) NOT NULL DEFAULT '1',
PRIMARY KEY (id)
);

# --- !Downs

DROP TABLE users;
12 changes: 12 additions & 0 deletions conf/evolutions/default/2.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Groups schema

# --- !Ups

CREATE TABLE groups (
id varchar(36) NOT NULL,
PRIMARY KEY (id)
);

# --- !Downs

DROP TABLE groups;
15 changes: 15 additions & 0 deletions conf/evolutions/default/3.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Groups schema

# --- !Ups

CREATE TABLE users_groups (
user_id varchar(36) NOT NULL,
group_id varchar(36) NOT NULL,
CONSTRAINT users_groups_users_fk FOREIGN KEY (user_id) REFERENCES users (id),
CONSTRAINT users_groups_groups_fk FOREIGN KEY (group_id) REFERENCES groups (id),
UNIQUE KEY user_id_group_id (user_id, group_id)
);

# --- !Downs

DROP TABLE users_groups;
22 changes: 22 additions & 0 deletions conf/evolutions/default/4.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Users test data

# --- !Ups

INSERT INTO users
(id, username, firstname, lastname, active)
VALUES
('1cda4679-3ed1-4439-b870-5132bbb286b2', 'gelliot', 'george', 'elliot', 1),
('286d06ef-28ca-4037-9215-331012b397c5', 'fidofido', 'fyodor', 'dostoevsky', 1),
('3f03dd45-946a-483c-9da6-4e930189dcef', 'jimbo123', 'james', 'levine', 1),
('8ef2fc7d-709b-4949-96c7-0c910393ad77', 'anonymouse', 'john', 'doe', 1),
('e38397c0-2f72-4238-8b15-c1477106c96e', 'vbeffa', 'vlad', 'beffa', 1);

# --- !Downs

DELETE FROM users WHERE id IN (
'1cda4679-3ed1-4439-b870-5132bbb286b2',
'286d06ef-28ca-4037-9215-331012b397c5',
'3f03dd45-946a-483c-9da6-4e930189dcef',
'8ef2fc7d-709b-4949-96c7-0c910393ad77',
'e38397c0-2f72-4238-8b15-c1477106c96e'
);
18 changes: 18 additions & 0 deletions conf/evolutions/default/5.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Groups test data

# --- !Ups

INSERT INTO groups
(id)
VALUES
('78cd42ed-e5f8-4378-9332-67d44b23b0cf'),
('84919ed5-3465-40da-9f60-114b9e7a470b'),
('b7786ae9-27d6-4679-8ab5-5c84b2f72a7a');

# --- !Downs

DELETE FROM groups WHERE id IN (
'78cd42ed-e5f8-4378-9332-67d44b23b0cf',
'84919ed5-3465-40da-9f60-114b9e7a470b',
'b7786ae9-27d6-4679-8ab5-5c84b2f72a7a'
);
Loading