Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(functions): add support for specifying function region #347

Merged
merged 3 commits into from
Apr 19, 2024
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
33 changes: 33 additions & 0 deletions Sources/Functions/FunctionsClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ public actor FunctionsClient {
let url: URL
/// Headers to be included in the requests.
var headers: [String: String]
/// The Region to invoke the functions in.
let region: String?
/// The fetch handler used to make requests.
let fetch: FetchHandler

Expand All @@ -26,17 +28,43 @@ public actor FunctionsClient {
/// - Parameters:
/// - url: The base URL for the functions.
/// - headers: Headers to be included in the requests. (Default: empty dictionary)
/// - region: The Region to invoke the functions in.
/// - fetch: The fetch handler used to make requests. (Default: URLSession.shared.data(for:))
@_disfavoredOverload
public init(
url: URL,
headers: [String: String] = [:],
region: String? = nil,
fetch: @escaping FetchHandler = { try await URLSession.shared.data(for: $0) }
) {
self.url = url
self.headers = headers
if headers["X-Client-Info"] == nil {
self.headers["X-Client-Info"] = "functions-swift/\(version)"
}
self.region = region
self.fetch = fetch
}

/// Initializes a new instance of `FunctionsClient`.
///
/// - Parameters:
/// - url: The base URL for the functions.
/// - headers: Headers to be included in the requests. (Default: empty dictionary)
/// - region: The Region to invoke the functions in.
/// - fetch: The fetch handler used to make requests. (Default: URLSession.shared.data(for:))
public init(
url: URL,
headers: [String: String] = [:],
region: FunctionRegion? = nil,
fetch: @escaping FetchHandler = { try await URLSession.shared.data(for: $0) }
) {
self.url = url
self.headers = headers
if headers["X-Client-Info"] == nil {
self.headers["X-Client-Info"] = "functions-swift/\(version)"
}
self.region = region?.rawValue
self.fetch = fetch
}

Expand Down Expand Up @@ -109,6 +137,11 @@ public actor FunctionsClient {
urlRequest.httpMethod = (invokeOptions.method ?? .post).rawValue
urlRequest.httpBody = invokeOptions.body

let region = invokeOptions.region ?? region
if let region {
urlRequest.setValue(region, forHTTPHeaderField: "x-region")
}

let (data, response) = try await fetch(urlRequest)

guard let httpResponse = response as? HTTPURLResponse else {
Expand Down
79 changes: 75 additions & 4 deletions Sources/Functions/Types.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,30 @@ public enum FunctionsError: Error, LocalizedError {
}

/// Options for invoking a function.
public struct FunctionInvokeOptions {
public struct FunctionInvokeOptions: Sendable {
/// Method to use in the function invocation.
let method: Method?
/// Headers to be included in the function invocation.
let headers: [String: String]
/// Body data to be sent with the function invocation.
let body: Data?
/// The Region to invoke the function in.
let region: String?

/// Initializes the `FunctionInvokeOptions` structure.
///
/// - Parameters:
/// - method: Method to use in the function invocation.
/// - headers: Headers to be included in the function invocation. (Default: empty dictionary)
/// - region: The Region to invoke the function in.
/// - body: The body data to be sent with the function invocation. (Default: nil)
public init(method: Method? = nil, headers: [String: String] = [:], body: some Encodable) {
@_disfavoredOverload
public init(
method: Method? = nil,
headers: [String: String] = [:],
region: String? = nil,
body: some Encodable
) {
var defaultHeaders = headers

switch body {
Expand All @@ -51,24 +60,86 @@ public struct FunctionInvokeOptions {

self.method = method
self.headers = defaultHeaders.merging(headers) { _, new in new }
self.region = region
}

/// Initializes the `FunctionInvokeOptions` structure.
///
/// - Parameters:
/// - method: Method to use in the function invocation.
/// - headers: Headers to be included in the function invocation. (Default: empty dictionary)
public init(method: Method? = nil, headers: [String: String] = [:]) {
/// - region: The Region to invoke the function in.
@_disfavoredOverload
public init(
method: Method? = nil,
headers: [String: String] = [:],
region: String? = nil
) {
self.method = method
self.headers = headers
self.region = region
body = nil
}

public enum Method: String {
public enum Method: String, Sendable {
case get = "GET"
case post = "POST"
case put = "PUT"
case patch = "PATCH"
case delete = "DELETE"
}
}

public enum FunctionRegion: String, Sendable {
case apNortheast1 = "ap-northeast-1"
case apNortheast2 = "ap-northeast-2"
case apSouth1 = "ap-south-1"
case apSoutheast1 = "ap-southeast-1"
case apSoutheast2 = "ap-southeast-2"
case caCentral1 = "ca-central-1"
case euCentral1 = "eu-central-1"
case euWest1 = "eu-west-1"
case euWest2 = "eu-west-2"
case euWest3 = "eu-west-3"
case saEast1 = "sa-east-1"
case usEast1 = "us-east-1"
case usWest1 = "us-west-1"
case usWest2 = "us-west-2"
}

extension FunctionInvokeOptions {
/// Initializes the `FunctionInvokeOptions` structure.
///
/// - Parameters:
/// - method: Method to use in the function invocation.
/// - headers: Headers to be included in the function invocation. (Default: empty dictionary)
/// - region: The Region to invoke the function in.
/// - body: The body data to be sent with the function invocation. (Default: nil)
public init(
method: Method? = nil,
headers: [String: String] = [:],
region: FunctionRegion? = nil,
body: some Encodable
) {
self.init(
method: method,
headers: headers,
region: region?.rawValue,
body: body
)
}

/// Initializes the `FunctionInvokeOptions` structure.
///
/// - Parameters:
/// - method: Method to use in the function invocation.
/// - headers: Headers to be included in the function invocation. (Default: empty dictionary)
/// - region: The Region to invoke the function in.
public init(
method: Method? = nil,
headers: [String: String] = [:],
region: FunctionRegion? = nil
) {
self.init(method: method, headers: headers, region: region?.rawValue)
}
}
1 change: 1 addition & 0 deletions Sources/Supabase/SupabaseClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ public final class SupabaseClient: @unchecked Sendable {
public private(set) lazy var functions = FunctionsClient(
url: functionsURL,
headers: defaultHeaders,
region: options.functions.region,
fetch: fetchWithAuth
)

Expand Down
23 changes: 21 additions & 2 deletions Sources/Supabase/Types.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ public struct SupabaseClientOptions: Sendable {
public let db: DatabaseOptions
public let auth: AuthOptions
public let global: GlobalOptions
public let functions: FunctionsOptions

public struct DatabaseOptions: Sendable {
/// The Postgres schema which your tables belong to. Must be on the list of exposed schemas in
Expand Down Expand Up @@ -87,26 +88,44 @@ public struct SupabaseClientOptions: Sendable {
}
}

public struct FunctionsOptions: Sendable {
/// The Region to invoke the functions in.
public let region: String?

@_disfavoredOverload
public init(region: String? = nil) {
self.region = region
}

public init(region: FunctionRegion? = nil) {
self.init(region: region?.rawValue)
}
}

public init(
db: DatabaseOptions = .init(),
auth: AuthOptions,
global: GlobalOptions = .init()
global: GlobalOptions = .init(),
functions: FunctionsOptions = .init()
) {
self.db = db
self.auth = auth
self.global = global
self.functions = functions
}
}

extension SupabaseClientOptions {
#if !os(Linux)
public init(
db: DatabaseOptions = .init(),
global: GlobalOptions = .init()
global: GlobalOptions = .init(),
functions: FunctionsOptions = .init()
) {
self.db = db
auth = .init()
self.global = global
self.functions = functions
}
#endif
}
Expand Down
47 changes: 47 additions & 0 deletions Tests/FunctionsTests/FunctionsClientTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,20 @@ final class FunctionsClientTests: XCTestCase {

lazy var sut = FunctionsClient(url: url, headers: ["Apikey": apiKey])

func testInit() async {
let client = FunctionsClient(
url: url,
headers: ["Apikey": apiKey],
region: .saEast1
)
let region = await client.region
XCTAssertEqual(region, "sa-east-1")

let headers = await client.headers
XCTAssertEqual(headers["Apikey"], apiKey)
XCTAssertNotNil(headers["X-Client-Info"])
}

func testInvoke() async throws {
let url = URL(string: "http://localhost:5432/functions/v1/hello_world")!
let _request = ActorIsolated(URLRequest?.none)
Expand Down Expand Up @@ -43,6 +57,39 @@ final class FunctionsClientTests: XCTestCase {
)
}

func testInvokeWithRegionDefinedInClient() async {
let sut = FunctionsClient(url: url, region: .caCentral1) {
let region = $0.value(forHTTPHeaderField: "x-region")
XCTAssertEqual(region, "ca-central-1")

throw CancellationError()
}

let _ = try? await sut.invoke("hello-world")
}

func testInvokeWithRegion() async {
let sut = FunctionsClient(url: url) {
let region = $0.value(forHTTPHeaderField: "x-region")
XCTAssertEqual(region, "ca-central-1")

throw CancellationError()
}

let _ = try? await sut.invoke("hello-world", options: .init(region: .caCentral1))
}

func testInvokeWithoutRegion() async {
let sut = FunctionsClient(url: url) {
let region = $0.value(forHTTPHeaderField: "x-region")
XCTAssertNil(region)

throw CancellationError()
}

let _ = try? await sut.invoke("hello-world")
}

func testInvoke_shouldThrow_URLError_badServerResponse() async {
let sut = FunctionsClient(url: url, headers: ["Apikey": apiKey]) { _ in
throw URLError(.badServerResponse)
Expand Down
9 changes: 8 additions & 1 deletion Tests/SupabaseTests/SupabaseClientTests.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Auth
import XCTest

@testable import Functions
@testable import Supabase

final class AuthLocalStorageMock: AuthLocalStorage {
Expand All @@ -14,7 +15,7 @@ final class AuthLocalStorageMock: AuthLocalStorage {
}

final class SupabaseClientTests: XCTestCase {
func testClientInitialization() {
func testClientInitialization() async {
let customSchema = "custom_schema"
let localStorage = AuthLocalStorageMock()
let customHeaders = ["header_field": "header_value"]
Expand All @@ -28,6 +29,9 @@ final class SupabaseClientTests: XCTestCase {
global: SupabaseClientOptions.GlobalOptions(
headers: customHeaders,
session: .shared
),
functions: SupabaseClientOptions.FunctionsOptions(
region: .apNortheast1
)
)
)
Expand All @@ -50,6 +54,9 @@ final class SupabaseClientTests: XCTestCase {
"Authorization": "Bearer ANON_KEY",
]
)

let region = await client.functions.region
XCTAssertEqual(region, "ap-northeast-1")
}

#if !os(Linux)
Expand Down
Loading