From bc808294ad85bd61853c3042ccfead27eec05891 Mon Sep 17 00:00:00 2001 From: Nicholas Kalar Date: Tue, 12 Aug 2025 22:23:03 -0400 Subject: [PATCH 1/9] Added initial read, create, and delete functions --- src/main/kotlin/service/LibraryService.kt | 54 ++++++++++++++++++++--- 1 file changed, 48 insertions(+), 6 deletions(-) diff --git a/src/main/kotlin/service/LibraryService.kt b/src/main/kotlin/service/LibraryService.kt index cef250f..922090c 100644 --- a/src/main/kotlin/service/LibraryService.kt +++ b/src/main/kotlin/service/LibraryService.kt @@ -2,25 +2,67 @@ package codes.kalar.service import codes.kalar.exception.DbElementInsertionException import codes.kalar.exception.DbElementNotFoundException +import codes.kalar.model.Library +import codes.kalar.model.NewLibrary import kotlinx.serialization.json.Json 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 SELECT_LIBRARY_BY_ID = "SELECT * FROM library WHERE id = ?" + private const val SELECT_LIBRARY_ALL = "SELECT * FROM library ORDER BY id DESC LIMIT 25" + private const val INSERT_LIBRARY = "INSERT INTO library (name, address) VALUES (?, ?)" private const val UPDATE_LIBRARY_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_LIBRARY_BY_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) + 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 read(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() {} + suspend fun update(library: Library) {} - suspend fun delete() {} + suspend fun delete(id: String) {} + + 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}") + } + } } From 88c3dfc59067fe6fc1d903f8f07d17188bfc8184 Mon Sep 17 00:00:00 2001 From: Nicholas Kalar Date: Tue, 12 Aug 2025 22:23:25 -0400 Subject: [PATCH 2/9] Added initial GET and POST endpoints --- src/main/kotlin/routes/LibraryRoutes.kt | 26 ++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/routes/LibraryRoutes.kt b/src/main/kotlin/routes/LibraryRoutes.kt index 622e10d..8322d09 100644 --- a/src/main/kotlin/routes/LibraryRoutes.kt +++ b/src/main/kotlin/routes/LibraryRoutes.kt @@ -1,17 +1,32 @@ 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.* import io.ktor.server.routing.* import java.sql.Connection +import java.sql.SQLException fun Application.configureLibraryRoutes(dbConnection: Connection) { + val libraryService = LibraryService(dbConnection) routing { get("/libraries") { - call.respondText("Libraries are neat!") + try { + val id = call.parameters["id"]?.toLong() ?: throw IllegalArgumentException("query parameter required") + val library = libraryService.read(id) + call.respond(HttpStatusCode.OK, library.toString()) + } 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/{libraryId}/items/{itemId}") { @@ -19,8 +34,13 @@ fun Application.configureLibraryRoutes(dbConnection: Connection) { } 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") { From bc0bdbdce00a236da7d3bef80f450637c88396ee Mon Sep 17 00:00:00 2001 From: Nicholas Kalar Date: Tue, 12 Aug 2025 22:23:45 -0400 Subject: [PATCH 3/9] Added missing variable. --- src/main/kotlin/model/Library.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/kotlin/model/Library.kt b/src/main/kotlin/model/Library.kt index e45a3e9..66e391e 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, ) @Serializable @@ -14,4 +15,5 @@ data class NewLibrary( // ID to be inserted by Database val name: String, val address: String, + val isArchived: Boolean, ) From 0e06cbaa1bf00c443c9878f96e46377ca449e816 Mon Sep 17 00:00:00 2001 From: Nicholas Kalar Date: Wed, 13 Aug 2025 15:11:42 -0400 Subject: [PATCH 4/9] fixed typo. --- src/main/resources/openapi/endpoints/items.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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" From 6ec38940251e4780e439e7e6c8da2f198da7f938 Mon Sep 17 00:00:00 2001 From: Nicholas Kalar Date: Wed, 13 Aug 2025 15:12:10 -0400 Subject: [PATCH 5/9] added bulk library GET --- src/main/kotlin/routes/LibraryRoutes.kt | 12 ++++++++---- src/main/kotlin/service/LibraryService.kt | 22 ++++++++++++++++++---- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/src/main/kotlin/routes/LibraryRoutes.kt b/src/main/kotlin/routes/LibraryRoutes.kt index 8322d09..5d9d8e0 100644 --- a/src/main/kotlin/routes/LibraryRoutes.kt +++ b/src/main/kotlin/routes/LibraryRoutes.kt @@ -11,7 +11,6 @@ import io.ktor.server.request.* import io.ktor.server.response.* import io.ktor.server.routing.* import java.sql.Connection -import java.sql.SQLException fun Application.configureLibraryRoutes(dbConnection: Connection) { val libraryService = LibraryService(dbConnection) @@ -19,9 +18,14 @@ fun Application.configureLibraryRoutes(dbConnection: Connection) { routing { get("/libraries") { try { - val id = call.parameters["id"]?.toLong() ?: throw IllegalArgumentException("query parameter required") - val library = libraryService.read(id) - call.respond(HttpStatusCode.OK, library.toString()) + 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) { diff --git a/src/main/kotlin/service/LibraryService.kt b/src/main/kotlin/service/LibraryService.kt index 922090c..59cda37 100644 --- a/src/main/kotlin/service/LibraryService.kt +++ b/src/main/kotlin/service/LibraryService.kt @@ -4,15 +4,15 @@ import codes.kalar.exception.DbElementInsertionException import codes.kalar.exception.DbElementNotFoundException import codes.kalar.model.Library import codes.kalar.model.NewLibrary -import kotlinx.serialization.json.Json import java.sql.* class LibraryService(private val connection: Connection) { companion object { private const val SELECT_LIBRARY_BY_ID = "SELECT * FROM library WHERE id = ?" + private const val SELECT_ALL_LIBRARIES = "SELECT * FROM library" private const val SELECT_LIBRARY_ALL = "SELECT * FROM library ORDER BY id DESC LIMIT 25" - private const val INSERT_LIBRARY = "INSERT INTO library (name, address) VALUES (?, ?)" + private const val INSERT_LIBRARY = "INSERT INTO library (name, address, is_archived) VALUES (?, ?, ?)" private const val UPDATE_LIBRARY_BY_ID = "" // In the event are "deleted" erroneously, having a flag set instead of actually removing the entry allows // for quick reversal. @@ -23,6 +23,7 @@ class LibraryService(private val connection: Connection) { 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 @@ -35,12 +36,12 @@ class LibraryService(private val connection: Connection) { } } - suspend fun read(id: Long): Library { + 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")) { + if (resultSet.next() && !resultSet.getBoolean("is_archived")) { return createLibraryFromResult(resultSet) } else { throw DbElementNotFoundException("Could not find collection item. resultSet: $resultSet") @@ -50,6 +51,19 @@ class LibraryService(private val connection: Connection) { } } + 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 update(library: Library) {} suspend fun delete(id: String) {} From e72452ffb3a4e2de33d2fdbe6b65272243c55392 Mon Sep 17 00:00:00 2001 From: Nicholas Kalar Date: Wed, 13 Aug 2025 15:12:25 -0400 Subject: [PATCH 6/9] Updated library doc --- src/main/resources/openapi/documentation.yaml | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) 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: From 5b1beabd9309c47ae6efcf75b5ea098b4747d6b8 Mon Sep 17 00:00:00 2001 From: Nicholas Kalar Date: Fri, 15 Aug 2025 14:27:13 -0400 Subject: [PATCH 7/9] Added missing field --- src/main/kotlin/model/Library.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/model/Library.kt b/src/main/kotlin/model/Library.kt index 66e391e..d3c3998 100644 --- a/src/main/kotlin/model/Library.kt +++ b/src/main/kotlin/model/Library.kt @@ -7,7 +7,7 @@ data class Library( val id: Long, val name: String, val address: String, - val isArchived: Boolean, + val isArchived: Boolean = false, ) @Serializable @@ -15,5 +15,5 @@ data class NewLibrary( // ID to be inserted by Database val name: String, val address: String, - val isArchived: Boolean, + val isArchived: Boolean = false, ) From 698f625fe26a1f93baa59f97826a11759058b5cd Mon Sep 17 00:00:00 2001 From: Nicholas Kalar Date: Fri, 15 Aug 2025 14:27:34 -0400 Subject: [PATCH 8/9] Created initial update and delete functions --- src/main/kotlin/service/LibraryService.kt | 32 ++++++++++++++++++----- 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/src/main/kotlin/service/LibraryService.kt b/src/main/kotlin/service/LibraryService.kt index 59cda37..2a1fafd 100644 --- a/src/main/kotlin/service/LibraryService.kt +++ b/src/main/kotlin/service/LibraryService.kt @@ -11,12 +11,11 @@ class LibraryService(private val connection: Connection) { companion object { private const val SELECT_LIBRARY_BY_ID = "SELECT * FROM library WHERE id = ?" private const val SELECT_ALL_LIBRARIES = "SELECT * FROM library" - private const val SELECT_LIBRARY_ALL = "SELECT * FROM library ORDER BY id DESC LIMIT 25" private const val INSERT_LIBRARY = "INSERT INTO library (name, address, is_archived) VALUES (?, ?, ?)" - private const val UPDATE_LIBRARY_BY_ID = "" + 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(library: NewLibrary): Long { @@ -64,11 +63,32 @@ class LibraryService(private val connection: Connection) { return libraries } - suspend fun update(library: Library) {} + 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: String) {} + 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") + } + } - fun createLibraryFromResult(resultSet: ResultSet): Library { + private fun createLibraryFromResult(resultSet: ResultSet): Library { try { val id = resultSet.getLong("id") val name = resultSet.getString("name") From 27ff5ed85e1732e6a185cf92f0ecd6c9a2e0754b Mon Sep 17 00:00:00 2001 From: Nicholas Kalar Date: Fri, 15 Aug 2025 14:28:12 -0400 Subject: [PATCH 9/9] Created initial patch and delete and updated get endpoints --- src/main/kotlin/routes/LibraryRoutes.kt | 44 ++++++++++++++++++++++--- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/routes/LibraryRoutes.kt b/src/main/kotlin/routes/LibraryRoutes.kt index 5d9d8e0..520cca4 100644 --- a/src/main/kotlin/routes/LibraryRoutes.kt +++ b/src/main/kotlin/routes/LibraryRoutes.kt @@ -33,8 +33,20 @@ fun Application.configureLibraryRoutes(dbConnection: Connection) { } } + 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") { @@ -48,12 +60,36 @@ fun Application.configureLibraryRoutes(dbConnection: Connection) { } 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") + } } }