diff --git a/Sources/PostgREST/PostgrestClient.swift b/Sources/PostgREST/PostgrestClient.swift index d342417ac..9c651c506 100644 --- a/Sources/PostgREST/PostgrestClient.swift +++ b/Sources/PostgREST/PostgrestClient.swift @@ -1,7 +1,7 @@ import ConcurrencyExtras import Foundation -import Helpers import HTTPTypes +import Helpers public typealias PostgrestError = Helpers.PostgrestError public typealias HTTPError = Helpers.HTTPError @@ -129,31 +129,69 @@ public final class PostgrestClient: Sendable { /// - Parameters: /// - fn: The function name to call. /// - params: The parameters to pass to the function call. + /// - head: When set to `true`, `data`, will not be returned. Useful if you only need the count. + /// - get: When set to `true`, the function will be called with read-only access mode. /// - count: Count algorithm to use to count rows returned by the function. Only applicable for [set-returning functions](https://www.postgresql.org/docs/current/functions-srf.html). public func rpc( _ fn: String, params: some Encodable & Sendable, + head: Bool = false, + get: Bool = false, count: CountOption? = nil ) throws -> PostgrestFilterBuilder { - try PostgrestRpcBuilder( + let method: HTTPTypes.HTTPRequest.Method + var url = configuration.url.appendingPathComponent("rpc/\(fn)") + let bodyData = try configuration.encoder.encode(params) + var body: Data? + + if head || get { + method = head ? .head : .get + + guard let json = try JSONSerialization.jsonObject(with: bodyData) as? [String: Any] else { + throw PostgrestError( + message: "Params should be a key-value type when using `GET` or `HEAD` options.") + } + + for (key, value) in json { + let formattedValue = (value as? [Any]).map(cleanFilterArray) ?? String(describing: value) + url.appendQueryItems([URLQueryItem(name: key, value: formattedValue)]) + } + + } else { + method = .post + body = bodyData + } + + var request = HTTPRequest( + url: url, + method: method, + headers: HTTPFields(configuration.headers), + body: params is NoParams ? nil : body + ) + + if let count { + request.headers[.prefer] = "count=\(count.rawValue)" + } + + return PostgrestFilterBuilder( configuration: configuration, - request: HTTPRequest( - url: configuration.url.appendingPathComponent("rpc/\(fn)"), - method: .post, - headers: HTTPFields(configuration.headers) - ) - ).rpc(params: params, count: count) + request: request + ) } /// Perform a function call. /// - Parameters: /// - fn: The function name to call. + /// - head: When set to `true`, `data`, will not be returned. Useful if you only need the count. + /// - get: When set to `true`, the function will be called with read-only access mode. /// - count: Count algorithm to use to count rows returned by the function. Only applicable for [set-returning functions](https://www.postgresql.org/docs/current/functions-srf.html). public func rpc( _ fn: String, + head: Bool = false, + get: Bool = false, count: CountOption? = nil ) throws -> PostgrestFilterBuilder { - try rpc(fn, params: NoParams(), count: count) + try rpc(fn, params: NoParams(), head: head, get: get, count: count) } /// Select a schema to query or perform an function (rpc) call. @@ -165,4 +203,14 @@ public final class PostgrestClient: Sendable { configuration.schema = schema return PostgrestClient(configuration: configuration) } + + private func cleanFilterArray(_ filter: [Any]) -> String { + "{\(filter.map { String(describing: $0) }.joined(separator: ","))}" + } +} + +struct NoParams: Encodable {} + +extension HTTPField.Name { + static let prefer = Self("Prefer")! } diff --git a/Sources/PostgREST/PostgrestFilterBuilder.swift b/Sources/PostgREST/PostgrestFilterBuilder.swift index 46ab06969..3abb64c22 100644 --- a/Sources/PostgREST/PostgrestFilterBuilder.swift +++ b/Sources/PostgREST/PostgrestFilterBuilder.swift @@ -1,7 +1,7 @@ import Foundation import Helpers -public class PostgrestFilterBuilder: PostgrestTransformBuilder { +public class PostgrestFilterBuilder: PostgrestTransformBuilder, @unchecked Sendable { public enum Operator: String, CaseIterable, Sendable { case eq, neq, gt, gte, lt, lte, like, ilike, `is`, `in`, cs, cd, sl, sr, nxl, nxr, adj, ov, fts, plfts, phfts, wfts diff --git a/Sources/PostgREST/PostgrestQueryBuilder.swift b/Sources/PostgREST/PostgrestQueryBuilder.swift index ed9efdf08..ee231a5fc 100644 --- a/Sources/PostgREST/PostgrestQueryBuilder.swift +++ b/Sources/PostgREST/PostgrestQueryBuilder.swift @@ -1,7 +1,7 @@ import Foundation import Helpers -public final class PostgrestQueryBuilder: PostgrestBuilder { +public final class PostgrestQueryBuilder: PostgrestBuilder, @unchecked Sendable { /// Perform a SELECT query on the table or view. /// - Parameters: /// - columns: The columns to retrieve, separated by commas. Columns can be renamed when returned with `customName:columnName` diff --git a/Sources/PostgREST/PostgrestRpcBuilder.swift b/Sources/PostgREST/PostgrestRpcBuilder.swift deleted file mode 100644 index c8232eba5..000000000 --- a/Sources/PostgREST/PostgrestRpcBuilder.swift +++ /dev/null @@ -1,48 +0,0 @@ -import Foundation -import Helpers -import HTTPTypes - -struct NoParams: Encodable {} - -public final class PostgrestRpcBuilder: PostgrestBuilder { - /// Performs a function call with parameters. - /// - Parameters: - /// - params: The parameters to pass to the function. - /// - head: When set to `true`, the function call will use the `HEAD` method. Default is - /// `false`. - /// - count: Count algorithm to use to count rows in a table. Default is `nil`. - /// - Returns: The `PostgrestTransformBuilder` instance for method chaining. - /// - Throws: An error if the function call fails. - func rpc( - params: some Encodable & Sendable, - head: Bool = false, - count: CountOption? = nil - ) throws -> PostgrestFilterBuilder { - // TODO: Support `HEAD` method - // https://github.com/supabase/postgrest-js/blob/master/src/lib/PostgrestRpcBuilder.ts#L38 - assert(head == false, "HEAD is not currently supported yet.") - - try mutableState.withValue { - $0.request.method = .post - if params is NoParams { - // noop - } else { - $0.request.body = try configuration.encoder.encode(params) - } - - if let count { - if let prefer = $0.request.headers[.prefer] { - $0.request.headers[.prefer] = "\(prefer),count=\(count.rawValue)" - } else { - $0.request.headers[.prefer] = "count=\(count.rawValue)" - } - } - } - - return PostgrestFilterBuilder(self) - } -} - -extension HTTPField.Name { - static let prefer = Self("Prefer")! -} diff --git a/Sources/PostgREST/PostgrestTransformBuilder.swift b/Sources/PostgREST/PostgrestTransformBuilder.swift index 89414f6ee..ead0cc0e9 100644 --- a/Sources/PostgREST/PostgrestTransformBuilder.swift +++ b/Sources/PostgREST/PostgrestTransformBuilder.swift @@ -1,7 +1,7 @@ import Foundation import Helpers -public class PostgrestTransformBuilder: PostgrestBuilder { +public class PostgrestTransformBuilder: PostgrestBuilder, @unchecked Sendable { /// Perform a SELECT on the query result. /// /// By default, `.insert()`, `.update()`, `.upsert()`, and `.delete()` do not return modified rows. By calling this method, modified rows are returned in `value`. diff --git a/Tests/IntegrationTests/Potsgrest/PostgresTransformsTests.swift b/Tests/IntegrationTests/Potsgrest/PostgresTransformsTests.swift index 68d7c7f01..a0c5925e0 100644 --- a/Tests/IntegrationTests/Potsgrest/PostgresTransformsTests.swift +++ b/Tests/IntegrationTests/Potsgrest/PostgresTransformsTests.swift @@ -14,14 +14,15 @@ final class PostgrestTransformsTests: XCTestCase { configuration: PostgrestClient.Configuration( url: URL(string: "\(DotEnv.SUPABASE_URL)/rest/v1")!, headers: [ - "apikey": DotEnv.SUPABASE_ANON_KEY, + "apikey": DotEnv.SUPABASE_ANON_KEY ], logger: nil ) ) func testOrder() async throws { - let res = try await client.from("users") + let res = + try await client.from("users") .select() .order("username", ascending: false) .execute().value as AnyJSON @@ -63,7 +64,8 @@ final class PostgrestTransformsTests: XCTestCase { } func testOrderOnMultipleColumns() async throws { - let res = try await client.from("messages") + let res = + try await client.from("messages") .select() .order("channel_id", ascending: false) .order("username", ascending: false) @@ -92,7 +94,8 @@ final class PostgrestTransformsTests: XCTestCase { } func testLimit() async throws { - let res = try await client.from("users") + let res = + try await client.from("users") .select() .limit(1) .execute().value as AnyJSON @@ -113,7 +116,8 @@ final class PostgrestTransformsTests: XCTestCase { } func testRange() async throws { - let res = try await client.from("users") + let res = + try await client.from("users") .select() .range(from: 1, to: 3) .execute().value as AnyJSON @@ -148,7 +152,8 @@ final class PostgrestTransformsTests: XCTestCase { } func testSingle() async throws { - let res = try await client.from("users") + let res = + try await client.from("users") .select() .limit(1) .single() @@ -168,7 +173,8 @@ final class PostgrestTransformsTests: XCTestCase { } func testSingleOnInsert() async throws { - let res = try await client.from("users") + let res = + try await client.from("users") .insert(["username": "foo"]) .select() .single() @@ -193,7 +199,8 @@ final class PostgrestTransformsTests: XCTestCase { } func testSelectOnInsert() async throws { - let res = try await client.from("users") + let res = + try await client.from("users") .insert(["username": "foo"]) .select("status") .execute().value as AnyJSON @@ -215,7 +222,8 @@ final class PostgrestTransformsTests: XCTestCase { } func testSelectOnRpc() async throws { - let res = try await client.rpc("get_username_and_status", params: ["name_param": "supabot"]) + let res = + try await client.rpc("get_username_and_status", params: ["name_param": "supabot"]) .select("status") .execute().value as AnyJSON @@ -230,6 +238,29 @@ final class PostgrestTransformsTests: XCTestCase { } } + func testRpcWithArray() async throws { + struct Params: Encodable { + let arr: [Int] + let index: Int + } + let res = + try await client.rpc("get_array_element", params: Params(arr: [37, 420, 64], index: 2)) + .execute().value as Int + XCTAssertEqual(res, 420) + } + + func testRpcWithReadOnlyAccessMode() async throws { + struct Params: Encodable { + let arr: [Int] + let index: Int + } + let res = + try await client.rpc( + "get_array_element", params: Params(arr: [37, 420, 64], index: 2), get: true + ).execute().value as Int + XCTAssertEqual(res, 420) + } + func testCsv() async throws { let res = try await client.from("users").select().csv().execute().string() assertInlineSnapshot(of: res, as: .json) { diff --git a/Tests/PostgRESTTests/BuildURLRequestTests.swift b/Tests/PostgRESTTests/BuildURLRequestTests.swift index 6bc1f80c5..c4ba68d1e 100644 --- a/Tests/PostgRESTTests/BuildURLRequestTests.swift +++ b/Tests/PostgRESTTests/BuildURLRequestTests.swift @@ -227,6 +227,19 @@ final class BuildURLRequestTests: XCTestCase { .select() .gt("created_at", value: Date(timeIntervalSince1970: 0)) }, + TestCase(name: "rpc call with head") { client in + try client.rpc("sum", head: true) + }, + TestCase(name: "rpc call with get") { client in + try client.rpc("sum", get: true) + }, + TestCase(name: "rpc call with get and params") { client in + try client.rpc( + "get_array_element", + params: ["array": [37, 420, 64], "index": 2] as AnyJSON, + get: true + ) + }, ] for testCase in testCases { diff --git a/Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.rpc-call-with-get-and-params.txt b/Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.rpc-call-with-get-and-params.txt new file mode 100644 index 000000000..fa22d49c8 --- /dev/null +++ b/Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.rpc-call-with-get-and-params.txt @@ -0,0 +1,5 @@ +curl \ + --header "Accept: application/json" \ + --header "Content-Type: application/json" \ + --header "X-Client-Info: postgrest-swift/x.y.z" \ + "https://example.supabase.co/rpc/get_array_element?array=%7B37,420,64%7D&index=2" \ No newline at end of file diff --git a/Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.rpc-call-with-get.txt b/Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.rpc-call-with-get.txt new file mode 100644 index 000000000..9fbc377a6 --- /dev/null +++ b/Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.rpc-call-with-get.txt @@ -0,0 +1,5 @@ +curl \ + --header "Accept: application/json" \ + --header "Content-Type: application/json" \ + --header "X-Client-Info: postgrest-swift/x.y.z" \ + "https://example.supabase.co/rpc/sum" \ No newline at end of file diff --git a/Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.rpc-call-with-head.txt b/Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.rpc-call-with-head.txt new file mode 100644 index 000000000..220929716 --- /dev/null +++ b/Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.rpc-call-with-head.txt @@ -0,0 +1,6 @@ +curl \ + --head \ + --header "Accept: application/json" \ + --header "Content-Type: application/json" \ + --header "X-Client-Info: postgrest-swift/x.y.z" \ + "https://example.supabase.co/rpc/sum" \ No newline at end of file