Skip to content

Commit f470874

Browse files
authored
feat(functions): add support for specifying function region (#347)
* feat(functions): add function region * feat: specify region when initializing client * chore: remove `any` function region and optional
1 parent 56c8117 commit f470874

File tree

6 files changed

+185
-7
lines changed

6 files changed

+185
-7
lines changed

Sources/Functions/FunctionsClient.swift

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ public actor FunctionsClient {
1818
let url: URL
1919
/// Headers to be included in the requests.
2020
var headers: [String: String]
21+
/// The Region to invoke the functions in.
22+
let region: String?
2123
/// The fetch handler used to make requests.
2224
let fetch: FetchHandler
2325

@@ -26,17 +28,43 @@ public actor FunctionsClient {
2628
/// - Parameters:
2729
/// - url: The base URL for the functions.
2830
/// - headers: Headers to be included in the requests. (Default: empty dictionary)
31+
/// - region: The Region to invoke the functions in.
2932
/// - fetch: The fetch handler used to make requests. (Default: URLSession.shared.data(for:))
33+
@_disfavoredOverload
3034
public init(
3135
url: URL,
3236
headers: [String: String] = [:],
37+
region: String? = nil,
3338
fetch: @escaping FetchHandler = { try await URLSession.shared.data(for: $0) }
3439
) {
3540
self.url = url
3641
self.headers = headers
3742
if headers["X-Client-Info"] == nil {
3843
self.headers["X-Client-Info"] = "functions-swift/\(version)"
3944
}
45+
self.region = region
46+
self.fetch = fetch
47+
}
48+
49+
/// Initializes a new instance of `FunctionsClient`.
50+
///
51+
/// - Parameters:
52+
/// - url: The base URL for the functions.
53+
/// - headers: Headers to be included in the requests. (Default: empty dictionary)
54+
/// - region: The Region to invoke the functions in.
55+
/// - fetch: The fetch handler used to make requests. (Default: URLSession.shared.data(for:))
56+
public init(
57+
url: URL,
58+
headers: [String: String] = [:],
59+
region: FunctionRegion? = nil,
60+
fetch: @escaping FetchHandler = { try await URLSession.shared.data(for: $0) }
61+
) {
62+
self.url = url
63+
self.headers = headers
64+
if headers["X-Client-Info"] == nil {
65+
self.headers["X-Client-Info"] = "functions-swift/\(version)"
66+
}
67+
self.region = region?.rawValue
4068
self.fetch = fetch
4169
}
4270

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

140+
let region = invokeOptions.region ?? region
141+
if let region {
142+
urlRequest.setValue(region, forHTTPHeaderField: "x-region")
143+
}
144+
112145
let (data, response) = try await fetch(urlRequest)
113146

114147
guard let httpResponse = response as? HTTPURLResponse else {

Sources/Functions/Types.swift

Lines changed: 75 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,21 +19,30 @@ public enum FunctionsError: Error, LocalizedError {
1919
}
2020

2121
/// Options for invoking a function.
22-
public struct FunctionInvokeOptions {
22+
public struct FunctionInvokeOptions: Sendable {
2323
/// Method to use in the function invocation.
2424
let method: Method?
2525
/// Headers to be included in the function invocation.
2626
let headers: [String: String]
2727
/// Body data to be sent with the function invocation.
2828
let body: Data?
29+
/// The Region to invoke the function in.
30+
let region: String?
2931

3032
/// Initializes the `FunctionInvokeOptions` structure.
3133
///
3234
/// - Parameters:
3335
/// - method: Method to use in the function invocation.
3436
/// - headers: Headers to be included in the function invocation. (Default: empty dictionary)
37+
/// - region: The Region to invoke the function in.
3538
/// - body: The body data to be sent with the function invocation. (Default: nil)
36-
public init(method: Method? = nil, headers: [String: String] = [:], body: some Encodable) {
39+
@_disfavoredOverload
40+
public init(
41+
method: Method? = nil,
42+
headers: [String: String] = [:],
43+
region: String? = nil,
44+
body: some Encodable
45+
) {
3746
var defaultHeaders = headers
3847

3948
switch body {
@@ -51,24 +60,86 @@ public struct FunctionInvokeOptions {
5160

5261
self.method = method
5362
self.headers = defaultHeaders.merging(headers) { _, new in new }
63+
self.region = region
5464
}
5565

5666
/// Initializes the `FunctionInvokeOptions` structure.
5767
///
5868
/// - Parameters:
5969
/// - method: Method to use in the function invocation.
6070
/// - headers: Headers to be included in the function invocation. (Default: empty dictionary)
61-
public init(method: Method? = nil, headers: [String: String] = [:]) {
71+
/// - region: The Region to invoke the function in.
72+
@_disfavoredOverload
73+
public init(
74+
method: Method? = nil,
75+
headers: [String: String] = [:],
76+
region: String? = nil
77+
) {
6278
self.method = method
6379
self.headers = headers
80+
self.region = region
6481
body = nil
6582
}
6683

67-
public enum Method: String {
84+
public enum Method: String, Sendable {
6885
case get = "GET"
6986
case post = "POST"
7087
case put = "PUT"
7188
case patch = "PATCH"
7289
case delete = "DELETE"
7390
}
7491
}
92+
93+
public enum FunctionRegion: String, Sendable {
94+
case apNortheast1 = "ap-northeast-1"
95+
case apNortheast2 = "ap-northeast-2"
96+
case apSouth1 = "ap-south-1"
97+
case apSoutheast1 = "ap-southeast-1"
98+
case apSoutheast2 = "ap-southeast-2"
99+
case caCentral1 = "ca-central-1"
100+
case euCentral1 = "eu-central-1"
101+
case euWest1 = "eu-west-1"
102+
case euWest2 = "eu-west-2"
103+
case euWest3 = "eu-west-3"
104+
case saEast1 = "sa-east-1"
105+
case usEast1 = "us-east-1"
106+
case usWest1 = "us-west-1"
107+
case usWest2 = "us-west-2"
108+
}
109+
110+
extension FunctionInvokeOptions {
111+
/// Initializes the `FunctionInvokeOptions` structure.
112+
///
113+
/// - Parameters:
114+
/// - method: Method to use in the function invocation.
115+
/// - headers: Headers to be included in the function invocation. (Default: empty dictionary)
116+
/// - region: The Region to invoke the function in.
117+
/// - body: The body data to be sent with the function invocation. (Default: nil)
118+
public init(
119+
method: Method? = nil,
120+
headers: [String: String] = [:],
121+
region: FunctionRegion? = nil,
122+
body: some Encodable
123+
) {
124+
self.init(
125+
method: method,
126+
headers: headers,
127+
region: region?.rawValue,
128+
body: body
129+
)
130+
}
131+
132+
/// Initializes the `FunctionInvokeOptions` structure.
133+
///
134+
/// - Parameters:
135+
/// - method: Method to use in the function invocation.
136+
/// - headers: Headers to be included in the function invocation. (Default: empty dictionary)
137+
/// - region: The Region to invoke the function in.
138+
public init(
139+
method: Method? = nil,
140+
headers: [String: String] = [:],
141+
region: FunctionRegion? = nil
142+
) {
143+
self.init(method: method, headers: headers, region: region?.rawValue)
144+
}
145+
}

Sources/Supabase/SupabaseClient.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ public final class SupabaseClient: @unchecked Sendable {
7070
public private(set) lazy var functions = FunctionsClient(
7171
url: functionsURL,
7272
headers: defaultHeaders,
73+
region: options.functions.region,
7374
fetch: fetchWithAuth
7475
)
7576

Sources/Supabase/Types.swift

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ public struct SupabaseClientOptions: Sendable {
1111
public let db: DatabaseOptions
1212
public let auth: AuthOptions
1313
public let global: GlobalOptions
14+
public let functions: FunctionsOptions
1415

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

91+
public struct FunctionsOptions: Sendable {
92+
/// The Region to invoke the functions in.
93+
public let region: String?
94+
95+
@_disfavoredOverload
96+
public init(region: String? = nil) {
97+
self.region = region
98+
}
99+
100+
public init(region: FunctionRegion? = nil) {
101+
self.init(region: region?.rawValue)
102+
}
103+
}
104+
90105
public init(
91106
db: DatabaseOptions = .init(),
92107
auth: AuthOptions,
93-
global: GlobalOptions = .init()
108+
global: GlobalOptions = .init(),
109+
functions: FunctionsOptions = .init()
94110
) {
95111
self.db = db
96112
self.auth = auth
97113
self.global = global
114+
self.functions = functions
98115
}
99116
}
100117

101118
extension SupabaseClientOptions {
102119
#if !os(Linux)
103120
public init(
104121
db: DatabaseOptions = .init(),
105-
global: GlobalOptions = .init()
122+
global: GlobalOptions = .init(),
123+
functions: FunctionsOptions = .init()
106124
) {
107125
self.db = db
108126
auth = .init()
109127
self.global = global
128+
self.functions = functions
110129
}
111130
#endif
112131
}

Tests/FunctionsTests/FunctionsClientTests.swift

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,20 @@ final class FunctionsClientTests: XCTestCase {
1313

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

16+
func testInit() async {
17+
let client = FunctionsClient(
18+
url: url,
19+
headers: ["Apikey": apiKey],
20+
region: .saEast1
21+
)
22+
let region = await client.region
23+
XCTAssertEqual(region, "sa-east-1")
24+
25+
let headers = await client.headers
26+
XCTAssertEqual(headers["Apikey"], apiKey)
27+
XCTAssertNotNil(headers["X-Client-Info"])
28+
}
29+
1630
func testInvoke() async throws {
1731
let url = URL(string: "http://localhost:5432/functions/v1/hello_world")!
1832
let _request = ActorIsolated(URLRequest?.none)
@@ -43,6 +57,39 @@ final class FunctionsClientTests: XCTestCase {
4357
)
4458
}
4559

60+
func testInvokeWithRegionDefinedInClient() async {
61+
let sut = FunctionsClient(url: url, region: .caCentral1) {
62+
let region = $0.value(forHTTPHeaderField: "x-region")
63+
XCTAssertEqual(region, "ca-central-1")
64+
65+
throw CancellationError()
66+
}
67+
68+
let _ = try? await sut.invoke("hello-world")
69+
}
70+
71+
func testInvokeWithRegion() async {
72+
let sut = FunctionsClient(url: url) {
73+
let region = $0.value(forHTTPHeaderField: "x-region")
74+
XCTAssertEqual(region, "ca-central-1")
75+
76+
throw CancellationError()
77+
}
78+
79+
let _ = try? await sut.invoke("hello-world", options: .init(region: .caCentral1))
80+
}
81+
82+
func testInvokeWithoutRegion() async {
83+
let sut = FunctionsClient(url: url) {
84+
let region = $0.value(forHTTPHeaderField: "x-region")
85+
XCTAssertNil(region)
86+
87+
throw CancellationError()
88+
}
89+
90+
let _ = try? await sut.invoke("hello-world")
91+
}
92+
4693
func testInvoke_shouldThrow_URLError_badServerResponse() async {
4794
let sut = FunctionsClient(url: url, headers: ["Apikey": apiKey]) { _ in
4895
throw URLError(.badServerResponse)

Tests/SupabaseTests/SupabaseClientTests.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import Auth
22
import XCTest
33

4+
@testable import Functions
45
@testable import Supabase
56

67
final class AuthLocalStorageMock: AuthLocalStorage {
@@ -14,7 +15,7 @@ final class AuthLocalStorageMock: AuthLocalStorage {
1415
}
1516

1617
final class SupabaseClientTests: XCTestCase {
17-
func testClientInitialization() {
18+
func testClientInitialization() async {
1819
let customSchema = "custom_schema"
1920
let localStorage = AuthLocalStorageMock()
2021
let customHeaders = ["header_field": "header_value"]
@@ -28,6 +29,9 @@ final class SupabaseClientTests: XCTestCase {
2829
global: SupabaseClientOptions.GlobalOptions(
2930
headers: customHeaders,
3031
session: .shared
32+
),
33+
functions: SupabaseClientOptions.FunctionsOptions(
34+
region: .apNortheast1
3135
)
3236
)
3337
)
@@ -50,6 +54,9 @@ final class SupabaseClientTests: XCTestCase {
5054
"Authorization": "Bearer ANON_KEY",
5155
]
5256
)
57+
58+
let region = await client.functions.region
59+
XCTAssertEqual(region, "ap-northeast-1")
5360
}
5461

5562
#if !os(Linux)

0 commit comments

Comments
 (0)