From b54073f91a4f6240ec5b67de467874c34bc95c9c Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Sun, 6 Aug 2023 10:01:32 -0300 Subject: [PATCH 1/5] Fix URL query encoding --- Package.resolved | 10 +-- Sources/PostgREST/PostgrestClient.swift | 70 +++++++++++++++++++ .../IntegrationTests.swift | 53 +++++++++++--- .../PostgRESTTests/BuildURLRequestTests.swift | 5 ++ .../testBuildRequest.query-with-character.txt | 4 ++ supabase/migrations/20220404094927_init.sql | 8 ++- 6 files changed, 133 insertions(+), 17 deletions(-) create mode 100644 Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.query-with-character.txt diff --git a/Package.resolved b/Package.resolved index fe1dee1..55cdff3 100644 --- a/Package.resolved +++ b/Package.resolved @@ -6,8 +6,8 @@ "repositoryURL": "https://github.com/kean/Get", "state": { "branch": null, - "revision": "7209fdb015686fd90918b2037cb2039206405bd3", - "version": "2.1.5" + "revision": "12830cc64f31789ae6f4352d2d51d03a25fc3741", + "version": "2.1.6" } }, { @@ -21,11 +21,11 @@ }, { "package": "swift-snapshot-testing", - "repositoryURL": "https://github.com/pointfreeco/swift-snapshot-testing.git", + "repositoryURL": "https://github.com/pointfreeco/swift-snapshot-testing", "state": { "branch": null, - "revision": "f29e2014f6230cf7d5138fc899da51c7f513d467", - "version": "1.10.0" + "revision": "dc46eeb3928a75390651fac6c1ef7f93ad59a73b", + "version": "1.11.1" } } ] diff --git a/Sources/PostgREST/PostgrestClient.swift b/Sources/PostgREST/PostgrestClient.swift index 92c5258..e9e5f16 100644 --- a/Sources/PostgREST/PostgrestClient.swift +++ b/Sources/PostgREST/PostgrestClient.swift @@ -94,6 +94,52 @@ struct PostgrestAPIClientDelegate: APIClientDelegate { throw try client.configuration.decoder.decode(PostgrestError.self, from: data) } + + func client(_ client: APIClient, makeURLForRequest request: Request) throws -> URL? { + func makeURL() -> URL? { + guard let url = request.url else { + return nil + } + + return url.scheme == nil ? client.configuration.baseURL? + .appendingPathComponent(url.absoluteString) : url + } + + guard let url = makeURL(), var components = URLComponents( + url: url, + resolvingAgainstBaseURL: false + ) else { + throw URLError(.badURL) + } + if let query = request.query, !query.isEmpty { + let percentEncodedQuery = (components.percentEncodedQuery.map { $0 + "&" } ?? "") + self + .query(query) + components.percentEncodedQuery = percentEncodedQuery + } + guard let url = components.url else { + throw URLError(.badURL) + } + return url + } + + private func escape(_ string: String) -> String { + string.addingPercentEncoding(withAllowedCharacters: .postgrestURLQueryAllowed) ?? string + } + + private func query(_ parameters: [(String, String?)]) -> String { + parameters.compactMap { key, value in + if let value { + return (key, value) + } + return nil + } + .map { key, value in + let escapedKey = escape(key) + let escapedValue = escape(value) + return "\(escapedKey)=\(escapedValue)" + } + .joined(separator: "&") + } } private let supportedDateFormatters: [ISO8601DateFormatter] = [ @@ -137,3 +183,27 @@ extension JSONEncoder { return encoder }() } + +extension CharacterSet { + /// Creates a CharacterSet from RFC 3986 allowed characters. + /// + /// RFC 3986 states that the following characters are "reserved" characters. + /// + /// - General Delimiters: ":", "#", "[", "]", "@", "?", "/" + /// - Sub-Delimiters: "!", "$", "&", "'", "(", ")", "*", "+", ",", ";", "=" + /// + /// In RFC 3986 - Section 3.4, it states that the "?" and "/" characters should not be escaped to + /// allow + /// query strings to include a URL. Therefore, all "reserved" characters with the exception of "?" + /// and "/" + /// should be percent-escaped in the query string. + static let postgrestURLQueryAllowed: CharacterSet = { + let generalDelimitersToEncode = + ":#[]@" // does not include "?" or "/" due to RFC 3986 - Section 3.4 + let subDelimitersToEncode = "!$&'()*+,;=" + let encodableDelimiters = + CharacterSet(charactersIn: "\(generalDelimitersToEncode)\(subDelimitersToEncode)") + + return CharacterSet.urlQueryAllowed.subtracting(encodableDelimiters) + }() +} diff --git a/Tests/PostgRESTIntegrationTests/IntegrationTests.swift b/Tests/PostgRESTIntegrationTests/IntegrationTests.swift index 85f5e83..60fcbc4 100644 --- a/Tests/PostgRESTIntegrationTests/IntegrationTests.swift +++ b/Tests/PostgRESTIntegrationTests/IntegrationTests.swift @@ -29,6 +29,10 @@ struct NewTodo: Codable, Hashable { } } +struct User: Codable, Hashable { + let email: String +} + @available(iOS 15.0.0, macOS 12.0.0, tvOS 13.0, *) final class IntegrationTests: XCTestCase { let client = PostgrestClient( @@ -50,14 +54,14 @@ final class IntegrationTests: XCTestCase { // Run fresh test by deleting all todos. Delete without a where clause isn't supported, so have // to do this `neq` trick to delete all data. - try await client.from("todo").delete().neq(column: "id", value: UUID().uuidString).execute() + try await client.from("todos").delete().neq(column: "id", value: UUID().uuidString).execute() } func testIntegration() async throws { - var todos: [Todo] = try await client.from("todo").select().execute().value + var todos: [Todo] = try await client.from("todos").select().execute().value XCTAssertEqual(todos, []) - let insertedTodo: Todo = try await client.from("todo") + let insertedTodo: Todo = try await client.from("todos") .insert( values: NewTodo( description: "Implement integration tests for postgrest-swift", @@ -69,10 +73,10 @@ final class IntegrationTests: XCTestCase { .execute() .value - todos = try await client.from("todo").select().execute().value + todos = try await client.from("todos").select().execute().value XCTAssertEqual(todos, [insertedTodo]) - let insertedTodos: [Todo] = try await client.from("todo") + let insertedTodos: [Todo] = try await client.from("todos") .insert( values: [ NewTodo(description: "Make supabase swift libraries production ready", tags: ["tag 01"]), @@ -83,11 +87,11 @@ final class IntegrationTests: XCTestCase { .execute() .value - todos = try await client.from("todo").select().execute().value + todos = try await client.from("todos").select().execute().value XCTAssertEqual(todos, [insertedTodo] + insertedTodos) let drinkCoffeeTodo = insertedTodos[1] - let updatedTodo: Todo = try await client.from("todo") + let updatedTodo: Todo = try await client.from("todos") .update(values: ["is_complete": true]) .eq(column: "id", value: drinkCoffeeTodo.id.uuidString) .single() @@ -95,19 +99,46 @@ final class IntegrationTests: XCTestCase { .value XCTAssertTrue(updatedTodo.isComplete) - let completedTodos: [Todo] = try await client.from("todo") + let completedTodos: [Todo] = try await client.from("todos") .select() .eq(column: "is_complete", value: true) .execute() .value XCTAssertEqual(completedTodos, [updatedTodo]) - try await client.from("todo").delete().eq(column: "is_complete", value: true).execute() - todos = try await client.from("todo").select().execute().value + try await client.from("todos").delete().eq(column: "is_complete", value: true).execute() + todos = try await client.from("todos").select().execute().value XCTAssertTrue(completedTodos.allSatisfy { todo in !todos.contains(todo) }) - let todosWithSpecificTag: [Todo] = try await client.from("todo").select() + let todosWithSpecificTag: [Todo] = try await client.from("todos").select() .contains(column: "tags", value: ["tag 01"]).execute().value XCTAssertEqual(todosWithSpecificTag, [insertedTodo, insertedTodos[0]]) } + + func testQueryWithPlusSign() async throws { + let users = [ + User(email: "johndoe@mail.com"), + User(email: "johndoe+test1@mail.com"), + User(email: "johndoe+test2@mail.com"), + ] + +// try await client.from("users").insert(values: users).execute() + + let fetchedUsers: [User] = try await client.from("users").select() + .ilike(column: "email", value: "johndoe+test%").execute().value + XCTAssertEqual( + fetchedUsers[...], + users[1 ... 2] + ) + } + + func testPercentEncodedString() { + let value = "johndoe+test%" + let percentEncoded = value + .addingPercentEncoding( + withAllowedCharacters: .urlQueryAllowed + .subtracting(CharacterSet(charactersIn: "+")) + ) + XCTAssertEqual(percentEncoded, "johndoe%2Btest%25") + } } diff --git a/Tests/PostgRESTTests/BuildURLRequestTests.swift b/Tests/PostgRESTTests/BuildURLRequestTests.swift index b7e851a..44a61a1 100644 --- a/Tests/PostgRESTTests/BuildURLRequestTests.swift +++ b/Tests/PostgRESTTests/BuildURLRequestTests.swift @@ -86,6 +86,11 @@ client.from("users") .upsert(values: ["email": "johndoe@supabase.io"], ignoreDuplicates: true) }, + TestCase(name: "query with + character") { client in + client.from("users") + .select() + .eq(column: "id", value: "Cigányka-ér (0+400 cskm) vízrajzi állomás") + }, ] for testCase in testCases { diff --git a/Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.query-with-character.txt b/Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.query-with-character.txt new file mode 100644 index 0000000..e814fea --- /dev/null +++ b/Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.query-with-character.txt @@ -0,0 +1,4 @@ +curl \ + --header "Accept: application/json" \ + --header "Content-Type: application/json" \ + "https://example.supabase.co/users?id=eq.Cig%C3%A1nyka-%C3%A9r%20(0+400%20cskm)%20v%C3%ADzrajzi%20%C3%A1llom%C3%A1s&select=*" \ No newline at end of file diff --git a/supabase/migrations/20220404094927_init.sql b/supabase/migrations/20220404094927_init.sql index e6c4565..79dd5d6 100644 --- a/supabase/migrations/20220404094927_init.sql +++ b/supabase/migrations/20220404094927_init.sql @@ -1,7 +1,13 @@ -create table todo ( +create table todos ( id uuid default uuid_generate_v4 () not null primary key, description text not null, is_complete boolean not null default false, tags text[], created_at timestamptz default (now() at time zone 'utc'::text) not null ); + +create table users ( + id uuid default uuid_generate_v4 () not null primary key, + email text not null, + created_at timestamptz default (now() at time zone 'utc'::text) not null +); From 5036bd1e323997560b66ae03473532d6f460df6e Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Fri, 6 Oct 2023 15:36:12 -0300 Subject: [PATCH 2/5] Fix integration test --- .../IntegrationTests.swift | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/Tests/PostgRESTIntegrationTests/IntegrationTests.swift b/Tests/PostgRESTIntegrationTests/IntegrationTests.swift index 60fcbc4..8f70357 100644 --- a/Tests/PostgRESTIntegrationTests/IntegrationTests.swift +++ b/Tests/PostgRESTIntegrationTests/IntegrationTests.swift @@ -52,9 +52,10 @@ final class IntegrationTests: XCTestCase { "INTEGRATION_TESTS not defined." ) - // Run fresh test by deleting all todos. Delete without a where clause isn't supported, so have + // Run fresh test by deleting all data. Delete without a where clause isn't supported, so have // to do this `neq` trick to delete all data. try await client.from("todos").delete().neq(column: "id", value: UUID().uuidString).execute() + try await client.from("users").delete().neq(column: "id", value: UUID().uuidString).execute() } func testIntegration() async throws { @@ -122,7 +123,7 @@ final class IntegrationTests: XCTestCase { User(email: "johndoe+test2@mail.com"), ] -// try await client.from("users").insert(values: users).execute() + try await client.from("users").insert(values: users).execute() let fetchedUsers: [User] = try await client.from("users").select() .ilike(column: "email", value: "johndoe+test%").execute().value @@ -131,14 +132,4 @@ final class IntegrationTests: XCTestCase { users[1 ... 2] ) } - - func testPercentEncodedString() { - let value = "johndoe+test%" - let percentEncoded = value - .addingPercentEncoding( - withAllowedCharacters: .urlQueryAllowed - .subtracting(CharacterSet(charactersIn: "+")) - ) - XCTAssertEqual(percentEncoded, "johndoe%2Btest%25") - } } From f49431b61fccbe3f2a9510ba0e882efb56249de3 Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Fri, 6 Oct 2023 15:36:52 -0300 Subject: [PATCH 3/5] Update dependencies --- Package.resolved | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/Package.resolved b/Package.resolved index 55cdff3..e541062 100644 --- a/Package.resolved +++ b/Package.resolved @@ -24,8 +24,17 @@ "repositoryURL": "https://github.com/pointfreeco/swift-snapshot-testing", "state": { "branch": null, - "revision": "dc46eeb3928a75390651fac6c1ef7f93ad59a73b", - "version": "1.11.1" + "revision": "506b6052384d8e97a4bb16fe8680325351c23c64", + "version": "1.14.0" + } + }, + { + "package": "swift-syntax", + "repositoryURL": "https://github.com/apple/swift-syntax.git", + "state": { + "branch": null, + "revision": "74203046135342e4a4a627476dd6caf8b28fe11b", + "version": "509.0.0" } } ] From 49975f69ceb848c03a43cd4eb33578466de5fc52 Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Fri, 6 Oct 2023 15:38:33 -0300 Subject: [PATCH 4/5] Simplify CI --- .github/workflows/ci.yml | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 469f9ac..1b1ed78 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,19 +14,8 @@ concurrency: jobs: library: - runs-on: macos-12 - strategy: - matrix: - xcode: ["14.1"] + runs-on: macos-latest steps: - uses: actions/checkout@v3 - - name: Select Xcode ${{ matrix.xcode }} - run: sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode }}.app - - uses: actions/cache@v3 - with: - path: .deriveddata - key: ${{ runner.os }}-spm-Xcode-${{ matrix.xcode }}-${{ hashFiles('**/Package.swift') }} - restore-keys: | - ${{ runner.os }}-spm-Xcode-${{ matrix.xcode }}- - name: Run tests run: make test-library From 2a6aef307b7b9b171091775a7991d6684dd26971 Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Fri, 6 Oct 2023 15:44:25 -0300 Subject: [PATCH 5/5] Trying to fix CI --- .github/workflows/ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1b1ed78..821d27c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,8 +14,10 @@ concurrency: jobs: library: - runs-on: macos-latest + runs-on: macos-13 steps: - uses: actions/checkout@v3 + - name: Select Xcode 14.3 + run: sudo xcode-select -s /Applications/Xcode_14.3.app - name: Run tests run: make test-library