diff --git a/src/main/kotlin/model/Library.kt b/src/main/kotlin/model/Library.kt index e45a3e9..d3c3998 100644 --- a/src/main/kotlin/model/Library.kt +++ b/src/main/kotlin/model/Library.kt @@ -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, ) diff --git a/src/main/kotlin/routes/LibraryRoutes.kt b/src/main/kotlin/routes/LibraryRoutes.kt index 622e10d..520cca4 100644 --- a/src/main/kotlin/routes/LibraryRoutes.kt +++ b/src/main/kotlin/routes/LibraryRoutes.kt @@ -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() - call.respondText("${library.name} is posted") + val library = call.receive() + 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() - call.respondText("${library.name} is patched") + try { + val library = call.receive() + 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") + } } } diff --git a/src/main/kotlin/service/LibraryService.kt b/src/main/kotlin/service/LibraryService.kt index cef250f..2a1fafd 100644 --- a/src/main/kotlin/service/LibraryService.kt +++ b/src/main/kotlin/service/LibraryService.kt @@ -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 { + val libraries = ArrayList() + 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}") + } + } } diff --git a/src/main/resources/openapi/documentation.yaml b/src/main/resources/openapi/documentation.yaml index 81f04e7..0a9754f 100644 --- a/src/main/resources/openapi/documentation.yaml +++ b/src/main/resources/openapi/documentation.yaml @@ -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: diff --git a/src/main/resources/openapi/endpoints/items.yaml b/src/main/resources/openapi/endpoints/items.yaml index 3406491..766b42b 100644 --- a/src/main/resources/openapi/endpoints/items.yaml +++ b/src/main/resources/openapi/endpoints/items.yaml @@ -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"