Merge pull request 'LMS-18 Library API' (#1) from FEAT-Library-API into main

Reviewed-on: https://gitea.com/NickKalar/LMS-APIs/pulls/1
This commit is contained in:
Nick Kalar
2025-08-15 18:44:13 +00:00
5 changed files with 174 additions and 22 deletions

View File

@@ -7,6 +7,7 @@ data class Library(
val id: Long,
val name: String,
val address: String,
val isArchived: Boolean = false,
)
@Serializable
@@ -14,4 +15,5 @@ data class NewLibrary(
// ID to be inserted by Database
val name: String,
val address: String,
val isArchived: Boolean = false,
)

View File

@@ -1,6 +1,11 @@
package codes.kalar.routes
import codes.kalar.exception.DbElementInsertionException
import codes.kalar.exception.DbElementNotFoundException
import codes.kalar.model.Library
import codes.kalar.model.NewLibrary
import codes.kalar.service.LibraryService
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.request.*
import io.ktor.server.response.*
@@ -8,28 +13,83 @@ import io.ktor.server.routing.*
import java.sql.Connection
fun Application.configureLibraryRoutes(dbConnection: Connection) {
val libraryService = LibraryService(dbConnection)
routing {
get("/libraries") {
call.respondText("Libraries are neat!")
try {
val id = call.parameters["id"]?.toLong()
if (id != null) {
val library = libraryService.readLibraryById(id)
call.respond(HttpStatusCode.OK, library)
} else {
val libraries = libraryService.readAllLibraries()
call.respond(HttpStatusCode.OK, libraries)
}
} catch (cause: DbElementNotFoundException) {
call.respond(HttpStatusCode.BadRequest, cause.message ?: "Unable to find Library.")
} catch (cause: IllegalArgumentException) {
call.respond(HttpStatusCode.BadRequest, cause.message ?: "Missing 'id' parameter.")
}
}
get("/libraries/{id}") {
try {
val id = call.pathParameters["id"]!!.toLong()
val library = libraryService.readLibraryById(id)
call.respond(HttpStatusCode.OK, library)
} catch (cause: DbElementNotFoundException) {
call.respond(HttpStatusCode.BadRequest, cause.message ?: "Unable to find Library.")
} catch (cause: NumberFormatException) {
call.respond(HttpStatusCode.BadRequest, "Unable to parse number format. \"${call.pathParameters["id"]}\" is not a number.")
}
}
get("/libraries/{libraryId}/items/{itemId}") {
call.respondText("You asked for ${call.parameters["itemId"]} from ${call.parameters["libraryId"]}")
// TODO Add search for collection_it where itemID && libraryID
}
post("/libraries") {
val library = call.receive<Library>()
call.respondText("${library.name} is posted")
val library = call.receive<NewLibrary>()
try {
val id = libraryService.create(library)
call.respondText("${library.name} is posted with the ID: $id")
} catch (cause: DbElementInsertionException) {
call.respond(HttpStatusCode.BadRequest, cause.message ?: "Unable to insert Library.")
}
}
patch("/libraries") {
val library = call.receive<Library>()
call.respondText("${library.name} is patched")
try {
val library = call.receive<Library>()
val patchedLibrary = libraryService.update(library)
call.respond(HttpStatusCode.OK, patchedLibrary)
} catch (cause: DbElementInsertionException) {
log.error(cause.message)
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) {
log.error(cause.message)
call.respond(HttpStatusCode.BadRequest, cause.message ?: "Bad Arguments")
}
}
delete("/libraries") {
call.respondText("We hate to see you go!")
try {
val id = call.parameters["id"]!!.toLong()
log.info("Deleting item with id=$id")
libraryService.delete(id)
call.respondText(":(", status = HttpStatusCode.OK)
} catch (cause: DbElementNotFoundException) {
log.error(cause.message, cause)
call.respond(HttpStatusCode.BadRequest, cause.message ?: "Bad Arguments")
} catch (cause: NumberFormatException) {
log.error(cause.message, cause)
call.respond(HttpStatusCode.BadRequest, cause.message ?: "Invalid ID format")
}
}
}

View File

@@ -2,25 +2,101 @@ package codes.kalar.service
import codes.kalar.exception.DbElementInsertionException
import codes.kalar.exception.DbElementNotFoundException
import kotlinx.serialization.json.Json
import codes.kalar.model.Library
import codes.kalar.model.NewLibrary
import java.sql.*
class LibraryService(private val connection: Connection) {
companion object {
private const val SELECT_LIBRARY_BY_ID = ""
private const val INSERT_LIBRARY = ""
private const val UPDATE_LIBRARY_BY_ID = ""
private const val SELECT_LIBRARY_BY_ID = "SELECT * FROM library WHERE id = ?"
private const val SELECT_ALL_LIBRARIES = "SELECT * FROM library"
private const val INSERT_LIBRARY = "INSERT INTO library (name, address, is_archived) VALUES (?, ?, ?)"
private const val UPDATE_LIBRARY_BY_ID = "UPDATE library SET name = ?, address = ? 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_LIBRARY_BY_ID = ""
private const val ARCHIVE_LIBRARY_BY_ID = "UPDATE library SET is_archived = true WHERE id = ?"
}
suspend fun create() {}
suspend fun create(library: NewLibrary): Long {
val statement = connection.prepareStatement(INSERT_LIBRARY, Statement.RETURN_GENERATED_KEYS)
statement.setString(1, library.name)
statement.setString(2, library.address)
statement.setBoolean(3, library.isArchived)
try {
statement.execute()
val key = statement.generatedKeys
if (key.next()) {
return key.getLong(1)
}
return -1
} catch (cause: SQLException) {
throw DbElementInsertionException("Could not insert library: ${cause.message}")
}
}
suspend fun read() {}
suspend fun readLibraryById(id: Long): Library {
val statement = connection.prepareStatement(SELECT_LIBRARY_BY_ID)
statement.setLong(1, id)
try {
val resultSet = statement.executeQuery()
if (resultSet.next() && !resultSet.getBoolean("is_archived")) {
return createLibraryFromResult(resultSet)
} else {
throw DbElementNotFoundException("Could not find collection item. resultSet: $resultSet")
}
} catch(cause: SQLException) {
throw DbElementNotFoundException("Could not find collection item with id $id")
}
}
suspend fun update() {}
fun readAllLibraries(): List<Library> {
val libraries = ArrayList<Library>()
val statement = connection.prepareStatement(SELECT_ALL_LIBRARIES)
val resultSet = statement.executeQuery()
while (resultSet.next()) {
if (!resultSet.getBoolean("is_archived")) {
val library = createLibraryFromResult(resultSet)
libraries.add(library)
}
}
return libraries
}
suspend fun delete() {}
suspend fun update(library: Library): Library {
val statement = connection.prepareStatement(UPDATE_LIBRARY_BY_ID)
try {
statement.setString(1, library.name)
statement.setString(2, library.address)
statement.setLong(3, library.id)
statement.execute()
return readLibraryById(library.id)
} catch (e: SQLException) {
throw DbElementInsertionException("${e.message}\ncollectionItem: $library\n statement: $statement\n ", e)
} catch (e: IllegalArgumentException) {
throw DbElementInsertionException("${e.message}\ncollectionItem: $library\n statement: $statement\n ", e)
}
}
suspend fun delete(id: Long) {
val statement = connection.prepareStatement(ARCHIVE_LIBRARY_BY_ID)
try {
statement.setLong(1, id)
statement.execute()
} catch (e: SQLException) {
throw DbElementNotFoundException("Could not find Library with id $id")
}
}
private fun createLibraryFromResult(resultSet: ResultSet): Library {
try {
val id = resultSet.getLong("id")
val name = resultSet.getString("name")
val address = resultSet.getString("address")
val isArchived = resultSet.getBoolean("is_archived")
return Library(id, name, address, isArchived)
} catch (cause: NullPointerException) {
throw DbElementInsertionException("${cause.message}\nresultSet = ${resultSet.metaData}")
}
}
}

View File

@@ -81,17 +81,31 @@ paths:
/libraries:
get:
description: ""
description: "Search for libraries based on ID or get all libraries"
parameters:
- name: "id"
in: "path"
required: true
required: false
schema:
type: string
example: id=27
example: id=1
responses:
"200":
description: "OK"
description: "Either a single library, or a list of all libraries."
"400":
description: "Bad Request. Likely, the ID you are looking for doesn't exist."
post:
description: "The method to add a new library."
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/NewLibrary"
responses:
"200":
description: "The ID of the newly created library will be returned."
"400":
description: "Bad Request. Unable to insert the new library."
components:
schemas:

View File

@@ -113,7 +113,7 @@ components:
example: ""
sortTitle:
type: "string"
description: "The tite of the book with any articles moved to the end for sorting purposes."
description: "The title of the book with any articles moved to the end for sorting purposes."
example: "Fellowship of the Ring, The"
format:
type: "string"
@@ -194,7 +194,7 @@ components:
example: ""
sortTitle:
type: "string"
description: "The tite of the book with any articles moved to the end for sorting purposes."
description: "The title of the book with any articles moved to the end for sorting purposes."
example: "Fellowship of the Ring, The"
format:
type: "string"