Merge pull request 'LMS- 20 Patron API' (#2) from FEAT-Patron-API into main
Reviewed-on: https://gitea.com/NickKalar/LMS-APIs/pulls/2
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
package codes.kalar.model
|
package codes.kalar.model
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
import java.sql.Date
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Patron(
|
data class Patron(
|
||||||
@@ -9,6 +10,7 @@ data class Patron(
|
|||||||
val hasGoodStanding: Boolean,
|
val hasGoodStanding: Boolean,
|
||||||
val feeTotal: Long,
|
val feeTotal: Long,
|
||||||
val isArchived: Boolean,
|
val isArchived: Boolean,
|
||||||
|
val loginUsername: String,
|
||||||
val lastLogin: String?,
|
val lastLogin: String?,
|
||||||
val password: String?,
|
val password: String?,
|
||||||
)
|
)
|
||||||
@@ -17,9 +19,10 @@ data class Patron(
|
|||||||
data class NewPatron(
|
data class NewPatron(
|
||||||
// ID to be inserted by Database
|
// ID to be inserted by Database
|
||||||
val name: String,
|
val name: String,
|
||||||
val hasGoodStanding: Boolean,
|
val hasGoodStanding: Boolean = true,
|
||||||
val feeTotal: Long,
|
val feeTotal: Long = 0,
|
||||||
val isArchived: Boolean,
|
val isArchived: Boolean = false,
|
||||||
val lastLogin: String?,
|
val loginUsername: String,
|
||||||
|
val lastLogin: String = Date(System.currentTimeMillis()).toString(),
|
||||||
val password: String?,
|
val password: String?,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -67,9 +67,6 @@ fun Application.configureLibraryRoutes(dbConnection: Connection) {
|
|||||||
} catch (cause: DbElementInsertionException) {
|
} catch (cause: DbElementInsertionException) {
|
||||||
log.error(cause.message)
|
log.error(cause.message)
|
||||||
call.respond(HttpStatusCode.BadRequest, cause.message ?: "Unable to update Library.")
|
call.respond(HttpStatusCode.BadRequest, cause.message ?: "Unable to update Library.")
|
||||||
} catch (cause: DbElementInsertionException) {
|
|
||||||
log.error(cause.message)
|
|
||||||
call.respond(HttpStatusCode.BadRequest, cause.message ?: "Bad Arguments")
|
|
||||||
} catch (cause: ContentTransformationException) {
|
} catch (cause: ContentTransformationException) {
|
||||||
log.error(cause.message)
|
log.error(cause.message)
|
||||||
call.respond(HttpStatusCode.BadRequest, cause.message ?: "Bad Arguments")
|
call.respond(HttpStatusCode.BadRequest, cause.message ?: "Bad Arguments")
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
package codes.kalar.routes
|
package codes.kalar.routes
|
||||||
|
|
||||||
|
import codes.kalar.exception.DbElementInsertionException
|
||||||
|
import codes.kalar.exception.DbElementNotFoundException
|
||||||
|
import codes.kalar.model.NewPatron
|
||||||
import codes.kalar.model.Patron
|
import codes.kalar.model.Patron
|
||||||
|
import codes.kalar.service.PatronService
|
||||||
import io.ktor.http.*
|
import io.ktor.http.*
|
||||||
import io.ktor.server.application.*
|
import io.ktor.server.application.*
|
||||||
import io.ktor.server.request.*
|
import io.ktor.server.request.*
|
||||||
@@ -9,32 +13,68 @@ import io.ktor.server.routing.*
|
|||||||
import java.sql.Connection
|
import java.sql.Connection
|
||||||
|
|
||||||
fun Application.configurePatronRoutes(dbConnection: Connection) {
|
fun Application.configurePatronRoutes(dbConnection: Connection) {
|
||||||
|
val patronService = PatronService(dbConnection)
|
||||||
|
|
||||||
routing {
|
routing {
|
||||||
get("/patron") {
|
get("/patron/{id}") {
|
||||||
if (call.request.queryParameters["patron"] == null) {
|
try {
|
||||||
call.respond(HttpStatusCode.BadRequest, "Invalid parameters")
|
val id = call.pathParameters["id"]!!.toLong()
|
||||||
|
val patron = patronService.readPatronById(id)
|
||||||
|
call.respond(HttpStatusCode.OK, patron)
|
||||||
|
} catch (cause: DbElementInsertionException) {
|
||||||
|
call.respond(HttpStatusCode.BadRequest, cause.message ?: "Unknown error")
|
||||||
|
} catch (cause: NumberFormatException) {
|
||||||
|
call.respond(HttpStatusCode.BadRequest, cause.message ?: "Unknown error")
|
||||||
}
|
}
|
||||||
else {
|
}
|
||||||
call.respondText("Hello, ${call.request.queryParameters["patron"]}")
|
|
||||||
|
get("/patron") {
|
||||||
|
try {
|
||||||
|
val username = call.parameters["username"]
|
||||||
|
if (username == null) {
|
||||||
|
call.respond(HttpStatusCode.BadRequest, "username & password required")
|
||||||
|
} else {
|
||||||
|
val patron = patronService.readPatronByLoginUsername(username)
|
||||||
|
call.respond(HttpStatusCode.OK, patron)
|
||||||
|
}
|
||||||
|
} catch (cause: DbElementNotFoundException) {
|
||||||
|
call.respond(HttpStatusCode.BadRequest, "Username not found")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
post("/patron") {
|
post("/patron") {
|
||||||
val patron = call.receive<Patron>()
|
try {
|
||||||
call.respondText("${patron.name} is posted")
|
val patron = call.receive<NewPatron>()
|
||||||
|
val id = patronService.create(patron)
|
||||||
|
call.respondText("Adding ${patron.name} to database with the id of $id", status = HttpStatusCode.OK)
|
||||||
|
} catch (cause: DbElementInsertionException) {
|
||||||
|
call.respond(HttpStatusCode.BadRequest, cause.message ?: "Bad Arguments")
|
||||||
|
} catch (cause: ContentTransformationException) {
|
||||||
|
call.respond(HttpStatusCode.BadRequest, "Bad Arguments. Must pass a valid CollectionItem object.")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
patch("/patron") {
|
patch("/patron") {
|
||||||
val patron = call.receive<Patron>()
|
try {
|
||||||
call.respondText("${patron.name} is patched")
|
val patron = call.receive<Patron>()
|
||||||
|
val patchedPatron = patronService.update(patron)
|
||||||
|
call.respondText("${patron.name} is patched")
|
||||||
|
} catch (cause: DbElementInsertionException) {
|
||||||
|
call.respond(HttpStatusCode.BadRequest, cause.message ?: "Unable to update Patron.")
|
||||||
|
} catch (cause: ContentTransformationException) {
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
delete("/patron") {
|
delete("/patron/{id}") {
|
||||||
if (call.request.queryParameters["id"] == null) {
|
try {
|
||||||
call.respond(HttpStatusCode.BadRequest, "Invalid parameters")
|
val id = call.pathParameters["id"]!!.toLong()
|
||||||
} else {
|
patronService.delete(id)
|
||||||
call.respondText("Do you have permissions?")
|
call.respond(HttpStatusCode.OK, "Successfully deleted the patron")
|
||||||
|
} catch (cause: DbElementInsertionException) {
|
||||||
|
call.respond(HttpStatusCode.BadRequest, cause.message ?: "Unable to delete Patron.")
|
||||||
|
} catch (cause: NumberFormatException) {
|
||||||
|
call.respond(HttpStatusCode.BadRequest, cause.message ?: "ID needs to be a number.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ import java.sql.*
|
|||||||
class CollectionItemService(private val connection: Connection) {
|
class CollectionItemService(private val connection: Connection) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val SELECT_ITEM_BY_TITLE = "SELECT * FROM collection_item WHERE levenshtein(title, ?) <= 5 LIMIT 25 OFFSET ?"
|
private const val SELECT_ITEM_BY_TITLE =
|
||||||
|
"SELECT * FROM collection_item WHERE levenshtein(title, ?) <= 5 LIMIT 25 OFFSET ?"
|
||||||
private const val SELECT_ITEM_BY_ID = "SELECT * FROM collection_item WHERE id = ?"
|
private const val SELECT_ITEM_BY_ID = "SELECT * FROM collection_item WHERE id = ?"
|
||||||
private const val INSERT_ITEM = "INSERT INTO collection_item (title, author, publisher, publishing_date, " +
|
private const val INSERT_ITEM = "INSERT INTO collection_item (title, author, publisher, publishing_date, " +
|
||||||
"loc_number, dewey_decimal_number, isbn, sort_title, format, language, page_count, categories, " +
|
"loc_number, dewey_decimal_number, isbn, sort_title, format, language, page_count, categories, " +
|
||||||
@@ -20,14 +21,17 @@ class CollectionItemService(private val connection: Connection) {
|
|||||||
"publishing_date = ?, loc_number = ?, dewey_decimal_number = ?, isbn = ?, sort_title = ?, format = ?, " +
|
"publishing_date = ?, loc_number = ?, dewey_decimal_number = ?, isbn = ?, sort_title = ?, format = ?, " +
|
||||||
"language = ?, page_count = ?, categories = ?, description = ?, price_in_cents = ?, cover_image_uri = ?, " +
|
"language = ?, page_count = ?, categories = ?, description = ?, price_in_cents = ?, cover_image_uri = ?, " +
|
||||||
"is_checked_in = ?, is_archived = ?, is_lost = ?, lost_date = ? WHERE id = ?"
|
"is_checked_in = ?, is_archived = ?, is_lost = ?, lost_date = ? WHERE id = ?"
|
||||||
|
|
||||||
// In the event books are "deleted" erroneously, having a flag set instead of actually removing the entry allows
|
// In the event books are "deleted" erroneously, having a flag set instead of actually removing the entry allows
|
||||||
// for quick reversal.
|
// for quick reversal.
|
||||||
private const val ARCHIVE_ITEM_BY_ID = "UPDATE collection_item SET is_archived = true WHERE id = ?"
|
private const val ARCHIVE_ITEM_BY_ID = "UPDATE collection_item SET is_archived = true WHERE id = ?"
|
||||||
}
|
}
|
||||||
|
|
||||||
fun create(newCollectionItem: NewCollectionItem): Long {
|
fun create(newCollectionItem: NewCollectionItem): Long {
|
||||||
val statement = connection.prepareStatement(INSERT_ITEM,
|
val statement = connection.prepareStatement(
|
||||||
Statement.RETURN_GENERATED_KEYS)
|
INSERT_ITEM,
|
||||||
|
Statement.RETURN_GENERATED_KEYS
|
||||||
|
)
|
||||||
statement.setString(1, newCollectionItem.title)
|
statement.setString(1, newCollectionItem.title)
|
||||||
statement.setString(2, newCollectionItem.author)
|
statement.setString(2, newCollectionItem.author)
|
||||||
statement.setString(3, newCollectionItem.publisher)
|
statement.setString(3, newCollectionItem.publisher)
|
||||||
@@ -47,7 +51,7 @@ class CollectionItemService(private val connection: Connection) {
|
|||||||
statement.setBoolean(17, newCollectionItem.isArchived)
|
statement.setBoolean(17, newCollectionItem.isArchived)
|
||||||
statement.setBoolean(18, newCollectionItem.isLost)
|
statement.setBoolean(18, newCollectionItem.isLost)
|
||||||
statement.setDate(19, Date.valueOf(newCollectionItem.lostDate))
|
statement.setDate(19, Date.valueOf(newCollectionItem.lostDate))
|
||||||
try{
|
try {
|
||||||
statement.execute()
|
statement.execute()
|
||||||
val key = statement.generatedKeys
|
val key = statement.generatedKeys
|
||||||
if (key.next()) {
|
if (key.next()) {
|
||||||
@@ -55,8 +59,10 @@ class CollectionItemService(private val connection: Connection) {
|
|||||||
}
|
}
|
||||||
return -1
|
return -1
|
||||||
} catch (cause: SQLException) {
|
} catch (cause: SQLException) {
|
||||||
throw DbElementInsertionException("Couldn't insert item " +
|
throw DbElementInsertionException(
|
||||||
"${newCollectionItem.title} into database. ${cause.message}")
|
"Couldn't insert item " +
|
||||||
|
"${newCollectionItem.title} into database. ${cause.message}"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,7 +97,7 @@ class CollectionItemService(private val connection: Connection) {
|
|||||||
} else {
|
} else {
|
||||||
throw DbElementNotFoundException("Could not find collection item. resultSet: $resultSet")
|
throw DbElementNotFoundException("Could not find collection item. resultSet: $resultSet")
|
||||||
}
|
}
|
||||||
} catch(cause: SQLException) {
|
} catch (cause: SQLException) {
|
||||||
throw DbElementNotFoundException("Could not find collection item with id $id")
|
throw DbElementNotFoundException("Could not find collection item with id $id")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -121,9 +127,15 @@ class CollectionItemService(private val connection: Connection) {
|
|||||||
statement.setLong(20, collectionItem.id)
|
statement.setLong(20, collectionItem.id)
|
||||||
return statement.execute()
|
return statement.execute()
|
||||||
} catch (e: SQLException) {
|
} catch (e: SQLException) {
|
||||||
throw DbElementInsertionException("${e.message}\ncollectionItem: $collectionItem\n statement: $statement\n ", e)
|
throw DbElementInsertionException(
|
||||||
|
"${e.message}\ncollectionItem: $collectionItem\n statement: $statement\n ",
|
||||||
|
e
|
||||||
|
)
|
||||||
} catch (e: IllegalArgumentException) {
|
} catch (e: IllegalArgumentException) {
|
||||||
throw DbElementInsertionException("${e.message}\ncollectionItem: $collectionItem\n statement: $statement\n ", e)
|
throw DbElementInsertionException(
|
||||||
|
"${e.message}\ncollectionItem: $collectionItem\n statement: $statement\n ",
|
||||||
|
e
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
package codes.kalar.service
|
|
||||||
|
|
||||||
import codes.kalar.exception.DbElementInsertionException
|
|
||||||
import codes.kalar.exception.DbElementNotFoundException
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import java.sql.*
|
|
||||||
|
|
||||||
class PartonService(private val connection: Connection) {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val SELECT_PATRON_BY_ = ""
|
|
||||||
private const val INSERT_PATRON = ""
|
|
||||||
private const val UPDATE_PATRON_BY_ID = ""
|
|
||||||
// In the event are "deleted" erroneously, having a flag set instead of actually removing the entry allows
|
|
||||||
// for quick reversal.
|
|
||||||
private const val ARCHIVE_PATRON_BY_ID = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun create() {}
|
|
||||||
|
|
||||||
suspend fun read() {}
|
|
||||||
|
|
||||||
suspend fun update() {}
|
|
||||||
|
|
||||||
suspend fun delete() {}
|
|
||||||
}
|
|
||||||
131
src/main/kotlin/service/PatronService.kt
Normal file
131
src/main/kotlin/service/PatronService.kt
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
package codes.kalar.service
|
||||||
|
|
||||||
|
import codes.kalar.exception.DbElementNotFoundException
|
||||||
|
import codes.kalar.model.NewPatron
|
||||||
|
import codes.kalar.model.Patron
|
||||||
|
import java.sql.*
|
||||||
|
|
||||||
|
class PatronService(private val connection: Connection) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val SELECT_PATRON_BY_ID = "SELECT * FROM patron WHERE id = ?"
|
||||||
|
private const val SELECT_PATRON_BY_LOGIN_USERNAME = "SELECT * From patron where login_username = ?"
|
||||||
|
private const val INSERT_PATRON =
|
||||||
|
"INSERT INTO patron (name, has_good_standing, fee_total, is_archived, login_username, last_login, password) VALUES (?, ?, ?, ?, ?, ?, ?)"
|
||||||
|
private const val UPDATE_PATRON_BY_ID =
|
||||||
|
"UPDATE patron SET name = ?, has_good_standing = ?, fee_total = ?, is_archived = ?, login_username = ?, " +
|
||||||
|
"last_login = ?, password = ? WHERE id = ?"
|
||||||
|
|
||||||
|
// In the event are "deleted" erroneously, having a flag set instead of actually removing the entry allows
|
||||||
|
// for quick reversal.
|
||||||
|
private const val ARCHIVE_PATRON_BY_ID = "UPDATE patron set is_archived = true WHERE id = ?"
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun create(newPatron: NewPatron): Long {
|
||||||
|
val statement = connection.prepareStatement(INSERT_PATRON, Statement.RETURN_GENERATED_KEYS)
|
||||||
|
statement.setString(1, newPatron.name)
|
||||||
|
statement.setBoolean(2, true)
|
||||||
|
statement.setLong(3, 0)
|
||||||
|
statement.setBoolean(4, false)
|
||||||
|
statement.setString(5, newPatron.loginUsername)
|
||||||
|
statement.setDate(6, Date(System.currentTimeMillis()))
|
||||||
|
statement.setString(7, newPatron.password)
|
||||||
|
statement.execute()
|
||||||
|
try {
|
||||||
|
val key = statement.generatedKeys
|
||||||
|
if (key.next()) {
|
||||||
|
return key.getLong(1)
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
} catch (cause: SQLException) {
|
||||||
|
throw DbElementNotFoundException("Couldn't insert patron ${newPatron.name} into database. ${cause.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun readPatronById(id: Long): Patron {
|
||||||
|
try {
|
||||||
|
val statement = connection.prepareStatement(SELECT_PATRON_BY_ID, Statement.RETURN_GENERATED_KEYS)
|
||||||
|
statement.setLong(1, id)
|
||||||
|
val resultSet = statement.executeQuery()
|
||||||
|
if (resultSet.next()) {
|
||||||
|
val patron = createPatronFromResults(resultSet)
|
||||||
|
return patron
|
||||||
|
} else {
|
||||||
|
throw DbElementNotFoundException("Patron with ID $id not found")
|
||||||
|
}
|
||||||
|
} catch (cause: SQLException) {
|
||||||
|
throw DbElementNotFoundException("Patron with ID $id is not found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun readPatronByLoginUsername(username: String): Patron {
|
||||||
|
try {
|
||||||
|
val statement = connection.prepareStatement(SELECT_PATRON_BY_LOGIN_USERNAME)
|
||||||
|
statement.setString(1, username)
|
||||||
|
val resultSet = statement.executeQuery()
|
||||||
|
if (resultSet.next()) {
|
||||||
|
val patron = createPatronFromResults(resultSet)
|
||||||
|
return patron
|
||||||
|
} else {
|
||||||
|
throw SQLException()
|
||||||
|
}
|
||||||
|
} catch (cause: SQLException) {
|
||||||
|
throw DbElementNotFoundException("Could not find username or password")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun loginPatronByLoginUsername(username: String, password: String): Boolean {
|
||||||
|
try {
|
||||||
|
val statement = connection.prepareStatement(SELECT_PATRON_BY_LOGIN_USERNAME)
|
||||||
|
statement.setString(1, username)
|
||||||
|
val resultSet = statement.executeQuery()
|
||||||
|
return if (resultSet.next()) {
|
||||||
|
resultSet.getString("password") == password
|
||||||
|
} else {
|
||||||
|
throw SQLException()
|
||||||
|
}
|
||||||
|
} catch (cause: SQLException) {
|
||||||
|
throw DbElementNotFoundException("Could not find username or password")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun update(patron: Patron): Boolean {
|
||||||
|
val statement = connection.prepareStatement(UPDATE_PATRON_BY_ID)
|
||||||
|
statement.setString(1, patron.name)
|
||||||
|
statement.setBoolean(2, patron.hasGoodStanding)
|
||||||
|
statement.setLong(3, patron.feeTotal)
|
||||||
|
statement.setBoolean(4, patron.isArchived)
|
||||||
|
statement.setString(5, patron.loginUsername)
|
||||||
|
statement.setString(6, patron.lastLogin)
|
||||||
|
statement.setString(7, patron.password)
|
||||||
|
statement.setLong(8, patron.id)
|
||||||
|
try {
|
||||||
|
return !statement.execute()
|
||||||
|
} catch (cause: SQLException) {
|
||||||
|
throw DbElementNotFoundException("Patron with ID ${patron.id} not found.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun delete(id: Long) {
|
||||||
|
val statement = connection.prepareStatement(ARCHIVE_PATRON_BY_ID)
|
||||||
|
statement.setLong(1, id)
|
||||||
|
try {
|
||||||
|
statement.execute()
|
||||||
|
} catch (cause: SQLException) {
|
||||||
|
throw DbElementNotFoundException("Patron with ID $id not found.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createPatronFromResults(resultSet: ResultSet): Patron {
|
||||||
|
return Patron(
|
||||||
|
id = resultSet.getLong("id"),
|
||||||
|
name = resultSet.getString("name"),
|
||||||
|
hasGoodStanding = resultSet.getBoolean("has_good_standing"),
|
||||||
|
feeTotal = resultSet.getLong("fee_total"),
|
||||||
|
isArchived = resultSet.getBoolean("is_archived"),
|
||||||
|
lastLogin = resultSet.getString("last_login"),
|
||||||
|
password = "", // For security reasons...
|
||||||
|
loginUsername = resultSet.getString("login_username")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user