Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 3 additions & 12 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,10 @@ concurrency:

jobs:
library:
runs-on: macos-12
strategy:
matrix:
xcode: ["14.1"]
runs-on: macos-13
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: Select Xcode 14.3
run: sudo xcode-select -s /Applications/Xcode_14.3.app
- name: Run tests
run: make test-library
19 changes: 14 additions & 5 deletions Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

70 changes: 70 additions & 0 deletions Sources/PostgREST/PostgrestClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,52 @@ struct PostgrestAPIClientDelegate: APIClientDelegate {

throw try client.configuration.decoder.decode(PostgrestError.self, from: data)
}

func client<T>(_ client: APIClient, makeURLForRequest request: Request<T>) 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] = [
Expand Down Expand Up @@ -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)
}()
}
46 changes: 34 additions & 12 deletions Tests/PostgRESTIntegrationTests/IntegrationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -48,16 +52,17 @@ 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("todo").delete().neq(column: "id", value: UUID().uuidString).execute()
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 {
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",
Expand All @@ -69,10 +74,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"]),
Expand All @@ -83,31 +88,48 @@ 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()
.execute()
.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: "[email protected]"),
User(email: "[email protected]"),
User(email: "[email protected]"),
]

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]
)
}
}
5 changes: 5 additions & 0 deletions Tests/PostgRESTTests/BuildURLRequestTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@
client.from("users")
.upsert(values: ["email": "[email protected]"], 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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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=*"
8 changes: 7 additions & 1 deletion supabase/migrations/20220404094927_init.sql
Original file line number Diff line number Diff line change
@@ -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
);