diff --git a/.github/workflows/build-ubuntu.yml b/.github/workflows/build-ubuntu.yml index 322f04a..bb991a2 100644 --- a/.github/workflows/build-ubuntu.yml +++ b/.github/workflows/build-ubuntu.yml @@ -11,7 +11,7 @@ jobs: name: Build on Ubuntu with Swift ${{matrix.swift}} strategy: matrix: - swift: [5.8.1, 5.7.3, 5.6.3] + swift: [5.9, 5.8.1, 5.7.3] runs-on: ubuntu-latest container: image: swift:${{matrix.swift}} diff --git a/Package.resolved b/Package.resolved index ad4dea0..390b3d2 100644 --- a/Package.resolved +++ b/Package.resolved @@ -5,8 +5,17 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/swift-server/async-http-client.git", "state" : { - "revision" : "16f7e62c08c6969899ce6cc277041e868364e5cf", - "version" : "1.19.0" + "revision" : "291438696abdd48d2a83b52465c176efbd94512b", + "version" : "1.20.1" + } + }, + { + "identity" : "swift-algorithms", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-algorithms", + "state" : { + "revision" : "f6919dfc309e7f1b56224378b11e28bab5bccc42", + "version" : "1.2.0" } }, { @@ -14,8 +23,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-atomics.git", "state" : { - "revision" : "6c89474e62719ddcc1e9614989fff2f68208fe10", - "version" : "1.1.0" + "revision" : "cd142fd2f64be2100422d658e7411e39489da985", + "version" : "1.2.0" } }, { @@ -23,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-collections.git", "state" : { - "revision" : "937e904258d22af6e447a0b72c0bc67583ef64a2", - "version" : "1.0.4" + "revision" : "d029d9d39c87bed85b1c50adee7c41795261a192", + "version" : "1.0.6" } }, { @@ -45,6 +54,15 @@ "version" : "1.0.0" } }, + { + "identity" : "swift-http-types", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-http-types", + "state" : { + "revision" : "1827dc94bdab2eb5f2fc804e9b0cb43574282566", + "version" : "1.0.2" + } + }, { "identity" : "swift-log", "kind" : "remoteSourceControl", @@ -59,8 +77,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio.git", "state" : { - "revision" : "cf281631ff10ec6111f2761052aa81896a83a007", - "version" : "2.58.0" + "revision" : "702cd7c56d5d44eeba73fdf83918339b26dc855c", + "version" : "2.62.0" } }, { @@ -68,8 +86,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio-extras.git", "state" : { - "revision" : "0e0d0aab665ff1a0659ce75ac003081f2b1c8997", - "version" : "1.19.0" + "revision" : "798c962495593a23fdea0c0c63fd55571d8dff51", + "version" : "1.20.0" } }, { @@ -77,8 +95,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio-http2.git", "state" : { - "revision" : "a8ccf13fa62775277a5d56844878c828bbb3be1a", - "version" : "1.27.0" + "revision" : "3bd9004b9d685ed6b629760fc84903e48efec806", + "version" : "1.29.0" } }, { @@ -95,8 +113,17 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio-transport-services.git", "state" : { - "revision" : "e7403c35ca6bb539a7ca353b91cc2d8ec0362d58", - "version" : "1.19.0" + "revision" : "ebf8b9c365a6ce043bf6e6326a04b15589bd285e", + "version" : "1.20.0" + } + }, + { + "identity" : "swift-numerics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-numerics.git", + "state" : { + "revision" : "0a5bc04095a675662cf24757cc0640aa2204253b", + "version" : "1.0.2" } } ], diff --git a/Package.swift b/Package.swift index 016478d..3c96474 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.6 +// swift-tools-version:5.7.1 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription diff --git a/README.md b/README.md index dc44aa3..70b826d 100644 --- a/README.md +++ b/README.md @@ -129,6 +129,13 @@ print(dbs) // prints: ["_global_changes", "_replicator", "_users", "yourDBname"] ``` +Find documents in DB by selector: +```swift +let selector = ["selector": ["name": "Sam"]] +let docs: [ExpectedDoc] = try await couchDBClient.find(in: "databaseName", selector: selector) +print(docs) +``` + ### Using with Vapor Here's a simple [tutorial](https://spaceinbox.me/docs/couchdbclient/tutorials/couchdbclient/vaportutorial) for Vapor. diff --git a/Sources/CouchDBClient/CouchDBClient.docc/CouchDBClient.md b/Sources/CouchDBClient/CouchDBClient.docc/CouchDBClient.md index 2e06c00..0672515 100644 --- a/Sources/CouchDBClient/CouchDBClient.docc/CouchDBClient.md +++ b/Sources/CouchDBClient/CouchDBClient.docc/CouchDBClient.md @@ -17,6 +17,7 @@ Currently CouchDBClient supports: - Get databases list. - Get document by id or documents using view. - Insert/update documents. +- Find documents by selector. - Delete documents. - CouchDB authorization. diff --git a/Sources/CouchDBClient/CouchDBClient.docc/Extensions/Client.md b/Sources/CouchDBClient/CouchDBClient.docc/Extensions/Client.md index 18ca725..65efbf7 100644 --- a/Sources/CouchDBClient/CouchDBClient.docc/Extensions/Client.md +++ b/Sources/CouchDBClient/CouchDBClient.docc/Extensions/Client.md @@ -27,5 +27,7 @@ A CouchDB client class with methods using Swift Concurrency. - ``insert(dbName:doc:dateEncodingStrategy:eventLoopGroup:)`` - ``update(dbName:doc:dateEncodingStrategy:eventLoopGroup:)`` - ``update(dbName:uri:body:eventLoopGroup:)`` +- ``find(in:body:eventLoopGroup:)`` +- ``find(in:selector:dateDecodingStrategy:eventLoopGroup:)`` - ``delete(fromDb:doc:eventLoopGroup:)`` - ``delete(fromDb:uri:rev:eventLoopGroup:)`` diff --git a/Sources/CouchDBClient/CouchDBClient.swift b/Sources/CouchDBClient/CouchDBClient.swift index 0b00432..f1bb924 100644 --- a/Sources/CouchDBClient/CouchDBClient.swift +++ b/Sources/CouchDBClient/CouchDBClient.swift @@ -22,6 +22,8 @@ public enum CouchDBClientError: Error { case insertError(error: CouchDBError) /// Update request wasn't successful. case updateError(error: CouchDBError) + /// Find request wasn't successful. + case findError(error: CouchDBError) /// Uknown response from CouchDB. case unknownResponse /// Wrong username or password. @@ -41,6 +43,8 @@ extension CouchDBClientError: LocalizedError { return "Insert request wasn't successful: \(error.localizedDescription)" case .updateError(let error): return "Update request wasn't successful: \(error.localizedDescription)" + case .findError(let error): + return "Find request wasn't successful: \(error.localizedDescription)" case .unknownResponse: return "Uknown response from CouchDB." case .unauthorized: @@ -478,6 +482,96 @@ public class CouchDBClient { throw parsingError } } + + /// Find data in DB by selector. + /// + /// Example: + /// + /// ```swift + /// // find documents in DB by selector + /// let selector = ["selector": ["name": "Sam"]] + /// let docs: [ExpectedDoc] = try await couchDBClient.find(in: testsDB, selector: selector) + /// ``` + /// + /// - Parameters: + /// - in dbName: DB name. + /// - selector: Codable representation of json selector query. + /// - eventLoopGroup: NIO's EventLoopGroup object. New will be created if nil value provided. + /// - Returns: Array of documents [T]. + public func find(in dbName: String, selector: Codable, dateDecodingStrategy: JSONDecoder.DateDecodingStrategy = .secondsSince1970, eventLoopGroup: EventLoopGroup? = nil) async throws -> [T] { + let encoder = JSONEncoder() + let selectorData = try encoder.encode(selector) + + let findResponse = try await find( + in: dbName, + body: .data(selectorData), + eventLoopGroup: eventLoopGroup + ) + + guard var body = findResponse.body, let bytes = body.readBytes(length: body.readableBytes) else { + throw CouchDBClientError.unknownResponse + } + + let data = Data(bytes) + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = dateDecodingStrategy + + do { + let doc = try decoder.decode(CouchDBFindResponse.self, from: data) + return doc.docs + } catch let parsingError { + if let couchdbError = try? decoder.decode(CouchDBError.self, from: data) { + throw CouchDBClientError.findError(error: couchdbError) + } + throw parsingError + } + } + + /// Find data in DB by selector. + /// + /// Example: + /// ```swift + /// let selector = ["selector": ["name": "Greg"]] + /// let bodyData = try JSONEncoder().encode(selector) + /// var findResponse = try await couchDBClient.find(in: testsDB, body: .data(bodyData)) + /// + /// let bytes = findResponse.body!.readBytes(length: findResponse.body!.readableBytes)! + /// let docs = try JSONDecoder().decode(CouchDBFindResponse.self, from: Data(bytes)).docs + /// ``` + /// - Parameters: + /// - dbName: DB name. + /// - body: Request body data. + /// - eventLoopGroup: NIO's EventLoopGroup object. New will be created if nil value provided. + /// - Returns: Request response. + public func find(in dbName: String, body: HTTPClient.Body, eventLoopGroup: EventLoopGroup? = nil) async throws -> HTTPClient.Response { + try await authIfNeed(eventLoopGroup: eventLoopGroup) + + let httpClient: HTTPClient + if let eventLoopGroup = eventLoopGroup { + httpClient = HTTPClient(eventLoopGroupProvider: .shared(eventLoopGroup)) + } else { + httpClient = HTTPClient(eventLoopGroupProvider: .singleton) + } + + defer { + DispatchQueue.main.async { + try? httpClient.syncShutdown() + } + } + + let url = buildUrl(path: "/" + dbName + "/_find", query: []) + var request = try buildRequest(fromUrl: url, withMethod: .POST) + request.body = body + let response = try await httpClient + .execute(request: request, deadline: .now() + .seconds(requestsTimeout)) + .get() + + if response.status == .unauthorized { + throw CouchDBClientError.unauthorized + } + + return response + } /// Update data in DB. /// diff --git a/Sources/CouchDBClient/Models/CouchDBFindResponse.swift b/Sources/CouchDBClient/Models/CouchDBFindResponse.swift new file mode 100644 index 0000000..cd40015 --- /dev/null +++ b/Sources/CouchDBClient/Models/CouchDBFindResponse.swift @@ -0,0 +1,13 @@ +// +// CouchDBFindResponse.swift +// +// +// Created by Gregorio Gevartosky Torrezan on 2023-11-15. +// + +import Foundation + +public struct CouchDBFindResponse: Codable { + var docs: [T] + var bookmark: String? +} diff --git a/Tests/CouchDBClientTests/CouchDBClientTests.swift b/Tests/CouchDBClientTests/CouchDBClientTests.swift index bf659d2..63b4c45 100644 --- a/Tests/CouchDBClientTests/CouchDBClientTests.swift +++ b/Tests/CouchDBClientTests/CouchDBClientTests.swift @@ -27,7 +27,7 @@ final class CouchDBClientTests: XCTestCase { try await super.setUp() } - func test0_CreateDB() async throws { + func test00_CreateDB() async throws { do { let exists = try await couchDBClient.dbExists(testsDB) if exists { @@ -40,7 +40,7 @@ final class CouchDBClientTests: XCTestCase { } } - func test1_DBExists() async throws { + func test01_DBExists() async throws { do { let exists = try await couchDBClient.dbExists(testsDB) XCTAssertTrue(exists) @@ -49,7 +49,7 @@ final class CouchDBClientTests: XCTestCase { } } - func test3_GetAllDbs() async throws { + func test03_GetAllDbs() async throws { do { let dbs = try await couchDBClient.getAllDBs() @@ -61,7 +61,7 @@ final class CouchDBClientTests: XCTestCase { } } - func test4_updateAndDeleteDocMethods() async throws { + func test04_updateAndDeleteDocMethods() async throws { var testDoc = ExpectedDoc(name: "test name") var expectedInsertId: String = "" var expectedInsertRev: String = "" @@ -141,7 +141,7 @@ final class CouchDBClientTests: XCTestCase { } } - func test5_InsertGetUpdateDelete() async throws { + func test05_InsertGetUpdateDelete() async throws { var testDoc = ExpectedDoc(name: "test name") var expectedInsertId: String = "" var expectedInsertRev: String = "" @@ -228,7 +228,7 @@ final class CouchDBClientTests: XCTestCase { } } - func test6_BuildUrl() { + func test06_BuildUrl() { let expectedUrl = "http://127.0.0.1:5984?key=testKey" let url = couchDBClient.buildUrl(path: "", query: [ URLQueryItem(name: "key", value: "testKey") @@ -236,13 +236,67 @@ final class CouchDBClientTests: XCTestCase { XCTAssertEqual(url, expectedUrl) } - func test7_Auth() async throws { + func test07_Auth() async throws { let session: CreateSessionResponse? = try await couchDBClient.authIfNeed() XCTAssertNotNil(session) XCTAssertEqual(true, session?.ok) XCTAssertNotNil(couchDBClient.sessionCookieExpires) } + func test08_find_with_body() async throws { + do { + let testDoc = ExpectedDoc(name: "Greg") + let insertEncodedData = try JSONEncoder().encode(testDoc) + let insertResponse = try await couchDBClient.insert( + dbName: testsDB, + body: .data(insertEncodedData) + ) + + let selector = ["selector": ["name": "Greg"]] + let bodyData = try JSONEncoder().encode(selector) + var findResponse = try await couchDBClient.find(in: testsDB, body: .data(bodyData)) + + let bytes = findResponse.body!.readBytes(length: findResponse.body!.readableBytes)! + let decodedResponse = try JSONDecoder().decode(CouchDBFindResponse.self, from: Data(bytes)) + + XCTAssertTrue(decodedResponse.docs.count > 0) + XCTAssertEqual(decodedResponse.docs.first!._id, insertResponse.id) + + _ = try await couchDBClient.delete( + fromDb: testsDB, + uri: decodedResponse.docs.first!._id!, + rev: decodedResponse.docs.first!._rev! + ) + } catch { + XCTFail(error.localizedDescription) + } + } + + func test09_find_with_generics() async throws { + do { + let testDoc = ExpectedDoc(name: "Sam") + let insertEncodedData = try JSONEncoder().encode(testDoc) + let insertResponse = try await couchDBClient.insert( + dbName: testsDB, + body: .data(insertEncodedData) + ) + + let selector = ["selector": ["name": "Sam"]] + let docs: [ExpectedDoc] = try await couchDBClient.find(in: testsDB, selector: selector) + + XCTAssertTrue(docs.count > 0) + XCTAssertEqual(docs.first!._id, insertResponse.id) + + _ = try await couchDBClient.delete( + fromDb: testsDB, + uri: docs.first!._id!, + rev: docs.first!._rev! + ) + } catch { + XCTFail(error.localizedDescription) + } + } + func test99_deleteDB() async throws { do { try await couchDBClient.deleteDB(testsDB)