From 1b370ffd52c0697e548386cb48f2cee59c88ff9f Mon Sep 17 00:00:00 2001 From: "zunda.dev@gmail.com" Date: Fri, 29 Nov 2024 20:48:43 +0900 Subject: [PATCH 1/3] replace HTTPRequest format --- Package.swift | 1 + Sources/Auth/AuthAdmin.swift | 45 +- Sources/Auth/AuthClient.swift | 559 ++++++++++-------- Sources/Auth/AuthClientConfiguration.swift | 31 +- Sources/Auth/AuthError.swift | 18 +- Sources/Auth/AuthMFA.swift | 59 +- Sources/Auth/Defaults.swift | 5 +- Sources/Auth/Deprecated.swift | 22 +- Sources/Auth/Internal/APIClient.swift | 72 ++- Sources/Auth/Internal/Contants.swift | 2 +- Sources/Auth/Internal/EventEmitter.swift | 5 +- .../Internal/FixedWidthInteger+Random.swift | 4 +- Sources/Auth/Internal/Keychain.swift | 7 +- Sources/Auth/Internal/SessionManager.swift | 21 +- Sources/Auth/Internal/SessionStorage.swift | 6 +- .../Auth/Storage/WinCredLocalStorage.swift | 2 +- Sources/Auth/Types.swift | 15 +- Sources/Functions/FunctionsClient.swift | 84 ++- Sources/Functions/Types.swift | 14 +- Sources/Helpers/FoundationExtensions.swift | 2 +- Sources/Helpers/HTTP/HTTPClient.swift | 40 +- Sources/Helpers/HTTP/HTTPFields.swift | 29 +- Sources/Helpers/HTTP/HTTPRequest.swift | 69 --- Sources/Helpers/HTTP/HTTPResponse.swift | 34 -- Sources/Helpers/HTTP/LoggerInterceptor.swift | 23 +- .../HTTP/RetryRequestInterceptor.swift | 30 +- Sources/Helpers/SharedModels/HTTPError.swift | 7 +- Sources/PostgREST/Defaults.swift | 5 +- Sources/PostgREST/Deprecated.swift | 26 +- Sources/PostgREST/PostgrestBuilder.swift | 42 +- Sources/PostgREST/PostgrestClient.swift | 53 +- .../PostgREST/PostgrestFilterBuilder.swift | 90 +-- Sources/PostgREST/PostgrestQueryBuilder.swift | 67 ++- .../PostgREST/PostgrestTransformBuilder.swift | 69 ++- Sources/PostgREST/Types.swift | 9 +- Sources/Realtime/Defaults.swift | 2 +- Sources/Realtime/Deprecated.swift | 5 +- Sources/Realtime/PhoenixTransport.swift | 4 +- Sources/Realtime/Presence.swift | 2 +- .../Realtime/RealtimeChannel+AsyncAwait.swift | 6 +- Sources/Realtime/RealtimeChannel.swift | 39 +- Sources/Realtime/RealtimeClient.swift | 30 +- Sources/Realtime/RealtimeMessage.swift | 4 +- Sources/Realtime/V2/RealtimeChannelV2.swift | 33 +- Sources/Realtime/V2/RealtimeClientV2.swift | 10 +- Sources/Realtime/V2/Types.swift | 20 +- Sources/Storage/Deprecated.swift | 3 +- Sources/Storage/Helpers.swift | 6 +- Sources/Storage/StorageApi.swift | 41 +- Sources/Storage/StorageBucketApi.swift | 87 +-- Sources/Storage/StorageFileApi.swift | 196 +++--- Sources/Storage/StorageHTTPClient.swift | 27 +- Sources/Storage/SupabaseStorage.swift | 5 +- Sources/Storage/Types.swift | 5 +- Sources/Supabase/SupabaseClient.swift | 80 ++- Sources/Supabase/Types.swift | 5 +- 56 files changed, 1182 insertions(+), 995 deletions(-) delete mode 100644 Sources/Helpers/HTTP/HTTPRequest.swift delete mode 100644 Sources/Helpers/HTTP/HTTPResponse.swift diff --git a/Package.swift b/Package.swift index 86d770cd..234975fa 100644 --- a/Package.swift +++ b/Package.swift @@ -35,6 +35,7 @@ let package = Package( dependencies: [ .product(name: "ConcurrencyExtras", package: "swift-concurrency-extras"), .product(name: "HTTPTypes", package: "swift-http-types"), + .product(name: "HTTPTypesFoundation", package: "swift-http-types"), ] ), .testTarget( diff --git a/Sources/Auth/AuthAdmin.swift b/Sources/Auth/AuthAdmin.swift index 1a31aee8..2c9fe873 100644 --- a/Sources/Auth/AuthAdmin.swift +++ b/Sources/Auth/AuthAdmin.swift @@ -6,8 +6,8 @@ // import Foundation -import Helpers import HTTPTypes +import Helpers public struct AuthAdmin: Sendable { let clientID: AuthClientID @@ -25,12 +25,12 @@ public struct AuthAdmin: Sendable { /// - Warning: Never expose your `service_role` key on the client. public func deleteUser(id: String, shouldSoftDelete: Bool = false) async throws { _ = try await api.execute( - HTTPRequest( - url: configuration.url.appendingPathComponent("admin/users/\(id)"), + for: HTTPRequest( method: .delete, - body: encoder.encode( - DeleteUserRequest(shouldSoftDelete: shouldSoftDelete) - ) + url: configuration.url.appendingPathComponent("admin/users/\(id)") + ), + from: encoder.encode( + DeleteUserRequest(shouldSoftDelete: shouldSoftDelete) ) ) } @@ -46,30 +46,35 @@ public struct AuthAdmin: Sendable { let aud: String } - let httpResponse = try await api.execute( - HTTPRequest( - url: configuration.url.appendingPathComponent("admin/users"), + let (data, response) = try await api.execute( + for: HTTPRequest( method: .get, - query: [ - URLQueryItem(name: "page", value: params?.page?.description ?? ""), - URLQueryItem(name: "per_page", value: params?.perPage?.description ?? ""), - ] - ) + url: configuration.url + .appendingPathComponent("admin/users") + .appendingQueryItems([ + URLQueryItem(name: "page", value: params?.page?.description ?? ""), + URLQueryItem(name: "per_page", value: params?.perPage?.description ?? ""), + ]) + ), + from: nil ) - let response = try httpResponse.decoded(as: Response.self, decoder: configuration.decoder) + let responseData = try configuration.decoder.decode(Response.self, from: data) var pagination = ListUsersPaginatedResponse( - users: response.users, - aud: response.aud, + users: responseData.users, + aud: responseData.aud, lastPage: 0, - total: httpResponse.headers[.xTotalCount].flatMap(Int.init) ?? 0 + total: response.headerFields[.xTotalCount].flatMap(Int.init) ?? 0 ) - let links = httpResponse.headers[.link]?.components(separatedBy: ",") ?? [] + let links = response.headerFields[.link]?.components(separatedBy: ",") ?? [] if !links.isEmpty { for link in links { - let page = link.components(separatedBy: ";")[0].components(separatedBy: "=")[1].prefix(while: \.isNumber) + let page = link + .components(separatedBy: ";")[0] + .components(separatedBy: "=")[1] + .prefix(while: \.isNumber) let rel = link.components(separatedBy: ";")[1].components(separatedBy: "=")[1] if rel == "\"last\"", let lastPage = Int(page) { diff --git a/Sources/Auth/AuthClient.swift b/Sources/Auth/AuthClient.swift index 2ca227c1..12dc3e43 100644 --- a/Sources/Auth/AuthClient.swift +++ b/Sources/Auth/AuthClient.swift @@ -1,5 +1,6 @@ import ConcurrencyExtras import Foundation +import HTTPTypes import Helpers #if canImport(AuthenticationServices) @@ -214,26 +215,28 @@ public final class AuthClient: Sendable { let (codeChallenge, codeChallengeMethod) = prepareForPKCE() return try await _signUp( - request: .init( - url: configuration.url.appendingPathComponent("signup"), + for: HTTPRequest( method: .post, - query: [ - (redirectTo ?? configuration.redirectToURL).map { - URLQueryItem( - name: "redirect_to", - value: $0.absoluteString - ) - } - ].compactMap { $0 }, - body: configuration.encoder.encode( - SignUpRequest( - email: email, - password: password, - data: data, - gotrueMetaSecurity: captchaToken.map(AuthMetaSecurity.init(captchaToken:)), - codeChallenge: codeChallenge, - codeChallengeMethod: codeChallengeMethod - ) + url: configuration.url + .appendingPathComponent("signup") + .appendingQueryItems( + [ + (redirectTo ?? configuration.redirectToURL).map { + URLQueryItem( + name: "redirect_to", + value: $0.absoluteString + ) + } + ].compactMap { $0 }) + ), + from: configuration.encoder.encode( + SignUpRequest( + email: email, + password: password, + data: data, + gotrueMetaSecurity: captchaToken.map(AuthMetaSecurity.init(captchaToken:)), + codeChallenge: codeChallenge, + codeChallengeMethod: codeChallengeMethod ) ) ) @@ -253,26 +256,28 @@ public final class AuthClient: Sendable { captchaToken: String? = nil ) async throws -> AuthResponse { try await _signUp( - request: .init( - url: configuration.url.appendingPathComponent("signup"), + for: HTTPRequest( method: .post, - body: configuration.encoder.encode( - SignUpRequest( - password: password, - phone: phone, - data: data, - gotrueMetaSecurity: captchaToken.map(AuthMetaSecurity.init(captchaToken:)) - ) + url: configuration.url.appendingPathComponent("signup") + ), + from: configuration.encoder.encode( + SignUpRequest( + password: password, + phone: phone, + data: data, + gotrueMetaSecurity: captchaToken.map(AuthMetaSecurity.init(captchaToken:)) ) ) ) } - private func _signUp(request: HTTPRequest) async throws -> AuthResponse { - let response = try await api.execute(request).decoded( - as: AuthResponse.self, - decoder: configuration.decoder - ) + private func _signUp( + for request: HTTPRequest, + from bodyData: Data? + ) async throws -> AuthResponse { + let (data, _) = try await api.execute(for: request, from: bodyData) + + let response = try configuration.decoder.decode(AuthResponse.self, from: data) if let session = response.session { await sessionManager.update(session) @@ -294,16 +299,17 @@ public final class AuthClient: Sendable { captchaToken: String? = nil ) async throws -> Session { try await _signIn( - request: .init( - url: configuration.url.appendingPathComponent("token"), + for: HTTPRequest( method: .post, - query: [URLQueryItem(name: "grant_type", value: "password")], - body: configuration.encoder.encode( - UserCredentials( - email: email, - password: password, - gotrueMetaSecurity: captchaToken.map(AuthMetaSecurity.init(captchaToken:)) - ) + url: configuration.url + .appendingPathComponent("token") + .appendingQueryItems([URLQueryItem(name: "grant_type", value: "password")]) + ), + from: configuration.encoder.encode( + UserCredentials( + email: email, + password: password, + gotrueMetaSecurity: captchaToken.map(AuthMetaSecurity.init(captchaToken:)) ) ) ) @@ -321,16 +327,17 @@ public final class AuthClient: Sendable { captchaToken: String? = nil ) async throws -> Session { try await _signIn( - request: .init( - url: configuration.url.appendingPathComponent("token"), + for: HTTPRequest( method: .post, - query: [URLQueryItem(name: "grant_type", value: "password")], - body: configuration.encoder.encode( - UserCredentials( - password: password, - phone: phone, - gotrueMetaSecurity: captchaToken.map(AuthMetaSecurity.init(captchaToken:)) - ) + url: configuration.url + .appendingPathComponent("token") + .appendingQueryItems([URLQueryItem(name: "grant_type", value: "password")]) + ), + from: configuration.encoder.encode( + UserCredentials( + password: password, + phone: phone, + gotrueMetaSecurity: captchaToken.map(AuthMetaSecurity.init(captchaToken:)) ) ) ) @@ -341,12 +348,13 @@ public final class AuthClient: Sendable { @discardableResult public func signInWithIdToken(credentials: OpenIDConnectCredentials) async throws -> Session { try await _signIn( - request: .init( - url: configuration.url.appendingPathComponent("token"), + for: HTTPRequest( method: .post, - query: [URLQueryItem(name: "grant_type", value: "id_token")], - body: configuration.encoder.encode(credentials) - ) + url: configuration.url + .appendingPathComponent("token") + .appendingQueryItems([URLQueryItem(name: "grant_type", value: "id_token")]) + ), + from: configuration.encoder.encode(credentials) ) } @@ -362,24 +370,23 @@ public final class AuthClient: Sendable { captchaToken: String? = nil ) async throws -> Session { try await _signIn( - request: HTTPRequest( - url: configuration.url.appendingPathComponent("signup"), + for: HTTPRequest( method: .post, - body: configuration.encoder.encode( - SignUpRequest( - data: data, - gotrueMetaSecurity: captchaToken.map { AuthMetaSecurity(captchaToken: $0) } - ) + url: configuration.url.appendingPathComponent("signup") + ), + from: configuration.encoder.encode( + SignUpRequest( + data: data, + gotrueMetaSecurity: captchaToken.map { AuthMetaSecurity(captchaToken: $0) } ) ) ) } - private func _signIn(request: HTTPRequest) async throws -> Session { - let session = try await api.execute(request).decoded( - as: Session.self, - decoder: configuration.decoder - ) + private func _signIn(for request: HTTPRequest, from bodyData: Data?) async throws -> Session { + let (data, _) = try await api.execute(for: request, from: bodyData) + + let session = try configuration.decoder.decode(Session.self, from: data) await sessionManager.update(session) eventEmitter.emit(.signedIn, session: session) @@ -408,26 +415,28 @@ public final class AuthClient: Sendable { let (codeChallenge, codeChallengeMethod) = prepareForPKCE() _ = try await api.execute( - .init( - url: configuration.url.appendingPathComponent("otp"), + for: HTTPRequest( method: .post, - query: [ - (redirectTo ?? configuration.redirectToURL).map { - URLQueryItem( - name: "redirect_to", - value: $0.absoluteString - ) - } - ].compactMap { $0 }, - body: configuration.encoder.encode( - OTPParams( - email: email, - createUser: shouldCreateUser, - data: data, - gotrueMetaSecurity: captchaToken.map(AuthMetaSecurity.init(captchaToken:)), - codeChallenge: codeChallenge, - codeChallengeMethod: codeChallengeMethod - ) + url: configuration.url + .appendingPathComponent("otp") + .appendingQueryItems( + [ + (redirectTo ?? configuration.redirectToURL).map { + URLQueryItem( + name: "redirect_to", + value: $0.absoluteString + ) + } + ].compactMap { $0 }) + ), + from: configuration.encoder.encode( + OTPParams( + email: email, + createUser: shouldCreateUser, + data: data, + gotrueMetaSecurity: captchaToken.map(AuthMetaSecurity.init(captchaToken:)), + codeChallenge: codeChallenge, + codeChallengeMethod: codeChallengeMethod ) ) ) @@ -451,17 +460,17 @@ public final class AuthClient: Sendable { captchaToken: String? = nil ) async throws { _ = try await api.execute( - .init( - url: configuration.url.appendingPathComponent("otp"), + for: HTTPRequest( method: .post, - body: configuration.encoder.encode( - OTPParams( - phone: phone, - createUser: shouldCreateUser, - channel: channel, - data: data, - gotrueMetaSecurity: captchaToken.map(AuthMetaSecurity.init(captchaToken:)) - ) + url: configuration.url.appendingPathComponent("otp") + ), + from: configuration.encoder.encode( + OTPParams( + phone: phone, + createUser: shouldCreateUser, + channel: channel, + data: data, + gotrueMetaSecurity: captchaToken.map(AuthMetaSecurity.init(captchaToken:)) ) ) ) @@ -480,23 +489,24 @@ public final class AuthClient: Sendable { ) async throws -> SSOResponse { let (codeChallenge, codeChallengeMethod) = prepareForPKCE() - return try await api.execute( - HTTPRequest( - url: configuration.url.appendingPathComponent("sso"), + let (data, _) = try await api.execute( + for: HTTPRequest( method: .post, - body: configuration.encoder.encode( - SignInWithSSORequest( - providerId: nil, - domain: domain, - redirectTo: redirectTo ?? configuration.redirectToURL, - gotrueMetaSecurity: captchaToken.map { AuthMetaSecurity(captchaToken: $0) }, - codeChallenge: codeChallenge, - codeChallengeMethod: codeChallengeMethod - ) + url: configuration.url.appendingPathComponent("sso") + ), + from: configuration.encoder.encode( + SignInWithSSORequest( + providerId: nil, + domain: domain, + redirectTo: redirectTo ?? configuration.redirectToURL, + gotrueMetaSecurity: captchaToken.map { AuthMetaSecurity(captchaToken: $0) }, + codeChallenge: codeChallenge, + codeChallengeMethod: codeChallengeMethod ) ) ) - .decoded(decoder: configuration.decoder) + + return try configuration.decoder.decode(SSOResponse.self, from: data) } /// Attempts a single-sign on using an enterprise Identity Provider. @@ -513,23 +523,24 @@ public final class AuthClient: Sendable { ) async throws -> SSOResponse { let (codeChallenge, codeChallengeMethod) = prepareForPKCE() - return try await api.execute( - HTTPRequest( - url: configuration.url.appendingPathComponent("sso"), + let (data, _) = try await api.execute( + for: HTTPRequest( method: .post, - body: configuration.encoder.encode( - SignInWithSSORequest( - providerId: providerId, - domain: nil, - redirectTo: redirectTo ?? configuration.redirectToURL, - gotrueMetaSecurity: captchaToken.map { AuthMetaSecurity(captchaToken: $0) }, - codeChallenge: codeChallenge, - codeChallengeMethod: codeChallengeMethod - ) + url: configuration.url.appendingPathComponent("sso") + ), + from: configuration.encoder.encode( + SignInWithSSORequest( + providerId: providerId, + domain: nil, + redirectTo: redirectTo ?? configuration.redirectToURL, + gotrueMetaSecurity: captchaToken.map { AuthMetaSecurity(captchaToken: $0) }, + codeChallenge: codeChallenge, + codeChallengeMethod: codeChallengeMethod ) ) ) - .decoded(decoder: configuration.decoder) + + return try configuration.decoder.decode(SSOResponse.self, from: data) } /// Log in an existing user by exchanging an Auth Code issued during the PKCE flow. @@ -541,20 +552,22 @@ public final class AuthClient: Sendable { "code verifier not found, a code verifier should exist when calling this method.") } - let session: Session = try await api.execute( - .init( - url: configuration.url.appendingPathComponent("token"), + let (data, _) = try await api.execute( + for: HTTPRequest( method: .post, - query: [URLQueryItem(name: "grant_type", value: "pkce")], - body: configuration.encoder.encode( - [ - "auth_code": authCode, - "code_verifier": codeVerifier, - ] - ) + url: configuration.url + .appendingPathComponent("token") + .appendingQueryItems([URLQueryItem(name: "grant_type", value: "pkce")]) + ), + from: configuration.encoder.encode( + [ + "auth_code": authCode, + "code_verifier": codeVerifier, + ] ) ) - .decoded(decoder: configuration.decoder) + + let session = try configuration.decoder.decode(Session.self, from: data) codeVerifierStorage.set(nil) @@ -797,13 +810,16 @@ public final class AuthClient: Sendable { let providerToken = params["provider_token"] let providerRefreshToken = params["provider_refresh_token"] - let user = try await api.execute( - .init( - url: configuration.url.appendingPathComponent("user"), + let (data, _) = try await api.execute( + for: HTTPRequest( method: .get, - headers: [.authorization: "\(tokenType) \(accessToken)"] - ) - ).decoded(as: User.self, decoder: configuration.decoder) + url: configuration.url.appendingPathComponent("user"), + headerFields: [.authorization: "\(tokenType) \(accessToken)"] + ), + from: nil + ) + + let user = try configuration.decoder.decode(User.self, from: data) let session = Session( providerToken: providerToken, @@ -903,15 +919,17 @@ public final class AuthClient: Sendable { do { _ = try await api.execute( - .init( - url: configuration.url.appendingPathComponent("logout"), + for: HTTPRequest( method: .post, - query: [URLQueryItem(name: "scope", value: scope.rawValue)], - headers: [.authorization: "Bearer \(accessToken)"] - ) + url: configuration.url + .appendingPathComponent("logout") + .appendingQueryItems([URLQueryItem(name: "scope", value: scope.rawValue)]), + headerFields: [.authorization: "Bearer \(accessToken)"] + ), + from: nil ) } catch let AuthError.api(_, _, _, response) - where [404, 403, 401].contains(response.statusCode) + where [404, 403, 401].contains(response.status.code) { // ignore 404s since user might not exist anymore // ignore 401s, and 403s since an invalid or expired JWT should sign out the current session. @@ -928,25 +946,27 @@ public final class AuthClient: Sendable { captchaToken: String? = nil ) async throws -> AuthResponse { try await _verifyOTP( - request: .init( - url: configuration.url.appendingPathComponent("verify"), + for: HTTPRequest( method: .post, - query: [ - (redirectTo ?? configuration.redirectToURL).map { - URLQueryItem( - name: "redirect_to", - value: $0.absoluteString - ) - } - ].compactMap { $0 }, - body: configuration.encoder.encode( - VerifyOTPParams.email( - VerifyEmailOTPParams( - email: email, - token: token, - type: type, - gotrueMetaSecurity: captchaToken.map(AuthMetaSecurity.init(captchaToken:)) - ) + url: configuration.url + .appendingPathComponent("verify") + .appendingQueryItems( + [ + (redirectTo ?? configuration.redirectToURL).map { + URLQueryItem( + name: "redirect_to", + value: $0.absoluteString + ) + } + ].compactMap { $0 }) + ), + from: configuration.encoder.encode( + VerifyOTPParams.email( + VerifyEmailOTPParams( + email: email, + token: token, + type: type, + gotrueMetaSecurity: captchaToken.map(AuthMetaSecurity.init(captchaToken:)) ) ) ) @@ -962,17 +982,17 @@ public final class AuthClient: Sendable { captchaToken: String? = nil ) async throws -> AuthResponse { try await _verifyOTP( - request: .init( - url: configuration.url.appendingPathComponent("verify"), + for: HTTPRequest( method: .post, - body: configuration.encoder.encode( - VerifyOTPParams.mobile( - VerifyMobileOTPParams( - phone: phone, - token: token, - type: type, - gotrueMetaSecurity: captchaToken.map(AuthMetaSecurity.init(captchaToken:)) - ) + url: configuration.url.appendingPathComponent("verify") + ), + from: configuration.encoder.encode( + VerifyOTPParams.mobile( + VerifyMobileOTPParams( + phone: phone, + token: token, + type: type, + gotrueMetaSecurity: captchaToken.map(AuthMetaSecurity.init(captchaToken:)) ) ) ) @@ -986,23 +1006,25 @@ public final class AuthClient: Sendable { type: EmailOTPType ) async throws -> AuthResponse { try await _verifyOTP( - request: .init( - url: configuration.url.appendingPathComponent("verify"), + for: HTTPRequest( method: .post, - body: configuration.encoder.encode( - VerifyOTPParams.tokenHash( - VerifyTokenHashParams(tokenHash: tokenHash, type: type) - ) + url: configuration.url.appendingPathComponent("verify") + ), + from: configuration.encoder.encode( + VerifyOTPParams.tokenHash( + VerifyTokenHashParams(tokenHash: tokenHash, type: type) ) ) ) } - private func _verifyOTP(request: HTTPRequest) async throws -> AuthResponse { - let response = try await api.execute(request).decoded( - as: AuthResponse.self, - decoder: configuration.decoder - ) + private func _verifyOTP( + for request: HTTPRequest, + from bodyData: Data? + ) async throws -> AuthResponse { + let (data, _) = try await api.execute(for: request, from: bodyData) + + let response = try configuration.decoder.decode(AuthResponse.self, from: data) if let session = response.session { await sessionManager.update(session) @@ -1023,23 +1045,25 @@ public final class AuthClient: Sendable { captchaToken: String? = nil ) async throws { _ = try await api.execute( - HTTPRequest( - url: configuration.url.appendingPathComponent("resend"), + for: HTTPRequest( method: .post, - query: [ - (emailRedirectTo ?? configuration.redirectToURL).map { - URLQueryItem( - name: "redirect_to", - value: $0.absoluteString - ) - } - ].compactMap { $0 }, - body: configuration.encoder.encode( - ResendEmailParams( - type: type, - email: email, - gotrueMetaSecurity: captchaToken.map(AuthMetaSecurity.init(captchaToken:)) - ) + url: configuration.url + .appendingPathComponent("resend") + .appendingQueryItems( + [ + (emailRedirectTo ?? configuration.redirectToURL).map { + URLQueryItem( + name: "redirect_to", + value: $0.absoluteString + ) + } + ].compactMap { $0 }) + ), + from: configuration.encoder.encode( + ResendEmailParams( + type: type, + email: email, + gotrueMetaSecurity: captchaToken.map(AuthMetaSecurity.init(captchaToken:)) ) ) ) @@ -1057,29 +1081,31 @@ public final class AuthClient: Sendable { type: ResendMobileType, captchaToken: String? = nil ) async throws -> ResendMobileResponse { - try await api.execute( - HTTPRequest( - url: configuration.url.appendingPathComponent("resend"), + let (data, _) = try await api.execute( + for: HTTPRequest( method: .post, - body: configuration.encoder.encode( - ResendMobileParams( - type: type, - phone: phone, - gotrueMetaSecurity: captchaToken.map(AuthMetaSecurity.init(captchaToken:)) - ) + url: configuration.url.appendingPathComponent("resend") + ), + from: configuration.encoder.encode( + ResendMobileParams( + type: type, + phone: phone, + gotrueMetaSecurity: captchaToken.map(AuthMetaSecurity.init(captchaToken:)) ) ) ) - .decoded(decoder: configuration.decoder) + + return try configuration.decoder.decode(ResendMobileResponse.self, from: data) } /// Sends a re-authentication OTP to the user's email or phone number. public func reauthenticate() async throws { try await api.authorizedExecute( - HTTPRequest( - url: configuration.url.appendingPathComponent("reauthenticate"), - method: .get - ) + for: HTTPRequest( + method: .get, + url: configuration.url.appendingPathComponent("reauthenticate") + ), + from: nil ) } @@ -1089,14 +1115,17 @@ public final class AuthClient: Sendable { /// /// Should be used only when you require the most current user data. For faster results, ``currentUser`` is recommended. public func user(jwt: String? = nil) async throws -> User { - var request = HTTPRequest(url: configuration.url.appendingPathComponent("user"), method: .get) + var request = HTTPRequest( + method: .get, + url: configuration.url.appendingPathComponent("user") + ) if let jwt { - request.headers[.authorization] = "Bearer \(jwt)" - return try await api.execute(request).decoded(decoder: configuration.decoder) + request.headerFields[.authorization] = "Bearer \(jwt)" } - return try await api.authorizedExecute(request).decoded(decoder: configuration.decoder) + let (data, _) = try await api.authorizedExecute(for: request, from: nil) + return try configuration.decoder.decode(User.self, from: data) } /// Updates user data, if there is a logged in user. @@ -1111,21 +1140,26 @@ public final class AuthClient: Sendable { } var session = try await sessionManager.session() - let updatedUser = try await api.authorizedExecute( - .init( - url: configuration.url.appendingPathComponent("user"), + let (data, _) = try await api.authorizedExecute( + for: HTTPRequest( method: .put, - query: [ - (redirectTo ?? configuration.redirectToURL).map { - URLQueryItem( - name: "redirect_to", - value: $0.absoluteString - ) - } - ].compactMap { $0 }, - body: configuration.encoder.encode(user) - ) - ).decoded(as: User.self, decoder: configuration.decoder) + url: configuration.url + .appendingPathComponent("user") + .appendingQueryItems( + [ + (redirectTo ?? configuration.redirectToURL).map { + URLQueryItem( + name: "redirect_to", + value: $0.absoluteString + ) + } + ].compactMap { $0 }) + ), + from: configuration.encoder.encode(user) + ) + + let updatedUser = try configuration.decoder.decode(User.self, from: data) + session.user = updatedUser await sessionManager.update(session) eventEmitter.emit(.userUpdated, session: session) @@ -1218,13 +1252,15 @@ public final class AuthClient: Sendable { let url: URL } - let response = try await api.authorizedExecute( - HTTPRequest( - url: url, - method: .get - ) + let (data, _) = try await api.authorizedExecute( + for: HTTPRequest( + method: .get, + url: url + ), + from: nil ) - .decoded(as: Response.self, decoder: configuration.decoder) + + let response = try configuration.decoder.decode(Response.self, from: data) return OAuthResponse(provider: provider, url: response.url) } @@ -1233,10 +1269,11 @@ public final class AuthClient: Sendable { /// with that identity once it's unlinked. public func unlinkIdentity(_ identity: UserIdentity) async throws { try await api.authorizedExecute( - HTTPRequest( - url: configuration.url.appendingPathComponent("user/identities/\(identity.identityId)"), - method: .delete - ) + for: HTTPRequest( + method: .delete, + url: configuration.url.appendingPathComponent("user/identities/\(identity.identityId)") + ), + from: nil ) } @@ -1249,24 +1286,26 @@ public final class AuthClient: Sendable { let (codeChallenge, codeChallengeMethod) = prepareForPKCE() _ = try await api.execute( - .init( - url: configuration.url.appendingPathComponent("recover"), + for: HTTPRequest( method: .post, - query: [ - (redirectTo ?? configuration.redirectToURL).map { - URLQueryItem( - name: "redirect_to", - value: $0.absoluteString - ) - } - ].compactMap { $0 }, - body: configuration.encoder.encode( - RecoverParams( - email: email, - gotrueMetaSecurity: captchaToken.map(AuthMetaSecurity.init(captchaToken:)), - codeChallenge: codeChallenge, - codeChallengeMethod: codeChallengeMethod - ) + url: configuration.url + .appendingPathComponent("recover") + .appendingQueryItems( + [ + (redirectTo ?? configuration.redirectToURL).map { + URLQueryItem( + name: "redirect_to", + value: $0.absoluteString + ) + } + ].compactMap { $0 }) + ), + from: configuration.encoder.encode( + RecoverParams( + email: email, + gotrueMetaSecurity: captchaToken.map(AuthMetaSecurity.init(captchaToken:)), + codeChallenge: codeChallenge, + codeChallengeMethod: codeChallengeMethod ) ) ) diff --git a/Sources/Auth/AuthClientConfiguration.swift b/Sources/Auth/AuthClientConfiguration.swift index 49b8577f..17289a85 100644 --- a/Sources/Auth/AuthClientConfiguration.swift +++ b/Sources/Auth/AuthClientConfiguration.swift @@ -6,6 +6,8 @@ // import Foundation +import HTTPTypes +import HTTPTypesFoundation import Helpers #if canImport(FoundationNetworking) @@ -15,8 +17,9 @@ import Helpers extension AuthClient { /// FetchHandler is a type alias for asynchronous network request handling. public typealias FetchHandler = @Sendable ( - _ request: URLRequest - ) async throws -> (Data, URLResponse) + _ request: HTTPRequest, + _ bodyData: Data? + ) async throws -> (Data, HTTPResponse) /// Configuration struct represents the client configuration. public struct Configuration: Sendable { @@ -24,7 +27,7 @@ extension AuthClient { public let url: URL /// Any additional headers to send to the Auth server. - public var headers: [String: String] + public var headers: HTTPFields public let flowType: AuthFlowType /// Default URL to be used for redirect on the flows that requires it. @@ -63,7 +66,7 @@ extension AuthClient { /// - autoRefreshToken: Set to `true` if you want to automatically refresh the token before expiring. public init( url: URL? = nil, - headers: [String: String] = [:], + headers: HTTPFields = [:], flowType: AuthFlowType = Configuration.defaultFlowType, redirectToURL: URL? = nil, storageKey: String? = nil, @@ -71,10 +74,16 @@ extension AuthClient { logger: (any SupabaseLogger)? = nil, encoder: JSONEncoder = AuthClient.Configuration.jsonEncoder, decoder: JSONDecoder = AuthClient.Configuration.jsonDecoder, - fetch: @escaping FetchHandler = { try await URLSession.shared.data(for: $0) }, + fetch: @escaping FetchHandler = { request, bodyData in + if let bodyData { + try await URLSession.shared.upload(for: request, from: bodyData) + } else { + try await URLSession.shared.data(for: request) + } + }, autoRefreshToken: Bool = AuthClient.Configuration.defaultAutoRefreshToken ) { - let headers = headers.merging(Configuration.defaultHeaders) { l, _ in l } + let headers = headers.merging(with: Configuration.defaultHeaders) self.url = url ?? defaultAuthURL self.headers = headers @@ -106,7 +115,7 @@ extension AuthClient { /// - autoRefreshToken: Set to `true` if you want to automatically refresh the token before expiring. public convenience init( url: URL? = nil, - headers: [String: String] = [:], + headers: HTTPFields = [:], flowType: AuthFlowType = AuthClient.Configuration.defaultFlowType, redirectToURL: URL? = nil, storageKey: String? = nil, @@ -114,7 +123,13 @@ extension AuthClient { logger: (any SupabaseLogger)? = nil, encoder: JSONEncoder = AuthClient.Configuration.jsonEncoder, decoder: JSONDecoder = AuthClient.Configuration.jsonDecoder, - fetch: @escaping FetchHandler = { try await URLSession.shared.data(for: $0) }, + fetch: @escaping FetchHandler = { request, bodyData in + if let bodyData { + try await URLSession.shared.upload(for: request, from: bodyData) + } else { + try await URLSession.shared.data(for: request) + } + }, autoRefreshToken: Bool = AuthClient.Configuration.defaultAutoRefreshToken ) { self.init( diff --git a/Sources/Auth/AuthError.swift b/Sources/Auth/AuthError.swift index e2f5278a..6d95fade 100644 --- a/Sources/Auth/AuthError.swift +++ b/Sources/Auth/AuthError.swift @@ -1,4 +1,5 @@ import Foundation +import HTTPTypes import Helpers #if canImport(FoundationNetworking) @@ -197,13 +198,8 @@ public enum AuthError: LocalizedError, Equatable { return .api( message: message, errorCode: .unknown, - underlyingData: (try? AuthClient.Configuration.jsonEncoder.encode(error)) ?? Data(), - underlyingResponse: HTTPURLResponse( - url: defaultAuthURL, - statusCode: error.code ?? 500, - httpVersion: nil, - headerFields: nil - )! + data: (try? AuthClient.Configuration.jsonEncoder.encode(error)) ?? Data(), + response: HTTPResponse(status: .init(code: error.code ?? 500)) ) } @@ -248,12 +244,12 @@ public enum AuthError: LocalizedError, Equatable { case weakPassword(message: String, reasons: [String]) /// Error thrown by API when an error occurs, check `errorCode` to know more, - /// or use `underlyingData` or `underlyingResponse` for access to the response which originated this error. + /// or use `data` or `response` for access to the response which originated this error. case api( message: String, errorCode: ErrorCode, - underlyingData: Data, - underlyingResponse: HTTPURLResponse + data: Data, + response: HTTPResponse ) /// Error thrown when an error happens during PKCE grant flow. @@ -303,7 +299,7 @@ extension AuthError: RetryableError { package var shouldRetry: Bool { switch self { case .api(_, _, _, let response): - defaultRetryableHTTPStatusCodes.contains(response.statusCode) + defaultRetryableHTTPStatusCodes.contains(response.status.code) default: false } } diff --git a/Sources/Auth/AuthMFA.swift b/Sources/Auth/AuthMFA.swift index 1c329e4d..fb6ac547 100644 --- a/Sources/Auth/AuthMFA.swift +++ b/Sources/Auth/AuthMFA.swift @@ -1,4 +1,5 @@ import Foundation +import HTTPTypes import Helpers /// Contains the full multi-factor authentication API. @@ -24,14 +25,15 @@ public struct AuthMFA: Sendable { /// - Parameter params: The parameters for enrolling a new MFA factor. /// - Returns: An authentication response after enrolling the factor. public func enroll(params: any MFAEnrollParamsType) async throws -> AuthMFAEnrollResponse { - try await api.authorizedExecute( - HTTPRequest( - url: configuration.url.appendingPathComponent("factors"), + let (data, _) = try await api.authorizedExecute( + for: HTTPRequest( method: .post, - body: encoder.encode(params) - ) + url: configuration.url.appendingPathComponent("factors") + ), + from: encoder.encode(params) ) - .decoded(decoder: decoder) + + return try decoder.decode(AuthMFAEnrollResponse.self, from: data) } /// Prepares a challenge used to verify that a user has access to a MFA factor. @@ -39,14 +41,15 @@ public struct AuthMFA: Sendable { /// - Parameter params: The parameters for creating a challenge. /// - Returns: An authentication response with the challenge information. public func challenge(params: MFAChallengeParams) async throws -> AuthMFAChallengeResponse { - try await api.authorizedExecute( - HTTPRequest( - url: configuration.url.appendingPathComponent("factors/\(params.factorId)/challenge"), + let (data, _) = try await api.authorizedExecute( + for: HTTPRequest( method: .post, - body: params.channel == nil ? nil : encoder.encode(["channel": params.channel]) - ) + url: configuration.url.appendingPathComponent("factors/\(params.factorId)/challenge") + ), + from: params.channel == nil ? nil : encoder.encode(["channel": params.channel]) ) - .decoded(decoder: decoder) + + return try decoder.decode(AuthMFAChallengeResponse.self, from: data) } /// Verifies a code against a challenge. The verification code is @@ -55,13 +58,15 @@ public struct AuthMFA: Sendable { /// - Parameter params: The parameters for verifying the MFA factor. /// - Returns: An authentication response after verifying the factor. public func verify(params: MFAVerifyParams) async throws -> AuthMFAVerifyResponse { - let response: AuthMFAVerifyResponse = try await api.authorizedExecute( - HTTPRequest( - url: configuration.url.appendingPathComponent("factors/\(params.factorId)/verify"), + let (data, _) = try await api.authorizedExecute( + for: HTTPRequest( method: .post, - body: encoder.encode(params) - ) - ).decoded(decoder: decoder) + url: configuration.url.appendingPathComponent("factors/\(params.factorId)/verify") + ), + from: encoder.encode(params) + ) + + let response: AuthMFAVerifyResponse = try decoder.decode(AuthMFAVerifyResponse.self, from: data) await sessionManager.update(response) @@ -77,13 +82,15 @@ public struct AuthMFA: Sendable { /// - Returns: An authentication response after unenrolling the factor. @discardableResult public func unenroll(params: MFAUnenrollParams) async throws -> AuthMFAUnenrollResponse { - try await api.authorizedExecute( - HTTPRequest( - url: configuration.url.appendingPathComponent("factors/\(params.factorId)"), - method: .delete - ) + let (data, _) = try await api.authorizedExecute( + for: HTTPRequest( + method: .delete, + url: configuration.url.appendingPathComponent("factors/\(params.factorId)") + ), + from: nil ) - .decoded(decoder: decoder) + + return try decoder.decode(AuthMFAUnenrollResponse.self, from: data) } /// Helper method which creates a challenge and immediately uses the given code to verify against @@ -122,7 +129,9 @@ public struct AuthMFA: Sendable { /// Returns the Authenticator Assurance Level (AAL) for the active session. /// /// - Returns: An authentication response with the Authenticator Assurance Level. - public func getAuthenticatorAssuranceLevel() async throws -> AuthMFAGetAuthenticatorAssuranceLevelResponse { + public func getAuthenticatorAssuranceLevel() async throws + -> AuthMFAGetAuthenticatorAssuranceLevelResponse + { do { let session = try await sessionManager.session() let payload = try decode(jwt: session.accessToken) diff --git a/Sources/Auth/Defaults.swift b/Sources/Auth/Defaults.swift index f293fe6b..9c999b92 100644 --- a/Sources/Auth/Defaults.swift +++ b/Sources/Auth/Defaults.swift @@ -7,6 +7,7 @@ import ConcurrencyExtras import Foundation +import HTTPTypes import Helpers extension AuthClient.Configuration { @@ -48,8 +49,8 @@ extension AuthClient.Configuration { return decoder }() - public static let defaultHeaders: [String: String] = [ - "X-Client-Info": "auth-swift/\(version)", + public static let defaultHeaders: HTTPFields = [ + .xClientInfo: "auth-swift/\(version)" ] /// The default ``AuthFlowType`` used when initializing a ``AuthClient`` instance. diff --git a/Sources/Auth/Deprecated.swift b/Sources/Auth/Deprecated.swift index ac7c1fca..1f7bd7b5 100644 --- a/Sources/Auth/Deprecated.swift +++ b/Sources/Auth/Deprecated.swift @@ -6,6 +6,8 @@ // import Foundation +import HTTPTypes +import HTTPTypesFoundation import Helpers #if canImport(FoundationNetworking) @@ -69,12 +71,18 @@ extension AuthClient.Configuration { ) public init( url: URL, - headers: [String: String] = [:], + headers: HTTPFields = [:], flowType: AuthFlowType = Self.defaultFlowType, localStorage: any AuthLocalStorage, encoder: JSONEncoder = AuthClient.Configuration.jsonEncoder, decoder: JSONDecoder = AuthClient.Configuration.jsonDecoder, - fetch: @escaping AuthClient.FetchHandler = { try await URLSession.shared.data(for: $0) } + fetch: @escaping AuthClient.FetchHandler = { request, bodyData in + if let bodyData { + try await URLSession.shared.upload(for: request, from: bodyData) + } else { + try await URLSession.shared.data(for: request) + } + } ) { self.init( url: url, @@ -107,12 +115,18 @@ extension AuthClient { ) public convenience init( url: URL, - headers: [String: String] = [:], + headers: HTTPFields = [:], flowType: AuthFlowType = Configuration.defaultFlowType, localStorage: any AuthLocalStorage, encoder: JSONEncoder = AuthClient.Configuration.jsonEncoder, decoder: JSONDecoder = AuthClient.Configuration.jsonDecoder, - fetch: @escaping AuthClient.FetchHandler = { try await URLSession.shared.data(for: $0) } + fetch: @escaping AuthClient.FetchHandler = { data, bodyData in + if let bodyData { + try await URLSession.shared.upload(for: data, from: bodyData) + } else { + try await URLSession.shared.data(for: data) + } + } ) { self.init( url: url, diff --git a/Sources/Auth/Internal/APIClient.swift b/Sources/Auth/Internal/APIClient.swift index fc2d8521..2a9e36c8 100644 --- a/Sources/Auth/Internal/APIClient.swift +++ b/Sources/Auth/Internal/APIClient.swift @@ -1,6 +1,6 @@ import Foundation -import Helpers import HTTPTypes +import Helpers extension HTTPClient { init(configuration: AuthClient.Configuration) { @@ -12,7 +12,7 @@ extension HTTPClient { interceptors.append( RetryRequestInterceptor( retryableHTTPMethods: RetryRequestInterceptor.defaultRetryableHTTPMethods.union( - [.post] // Add POST method so refresh token are also retried. + [.post] // Add POST method so refresh token are also retried. ) ) ) @@ -32,25 +32,34 @@ struct APIClient: Sendable { Dependencies[clientID].http } - func execute(_ request: Helpers.HTTPRequest) async throws -> Helpers.HTTPResponse { + func execute( + for request: HTTPRequest, + from bodyData: Data? + ) async throws -> ( + Data, + HTTPResponse + ) { var request = request - request.headers = HTTPFields(configuration.headers).merging(with: request.headers) + request.headerFields = HTTPFields(configuration.headers).merging(with: request.headerFields) - if request.headers[.apiVersionHeaderName] == nil { - request.headers[.apiVersionHeaderName] = apiVersions[._20240101]!.name.rawValue + if request.headerFields[.apiVersionHeaderName] == nil { + request.headerFields[.apiVersionHeaderName] = apiVersions[._20240101]!.name.rawValue } - let response = try await http.send(request) + let (data, response) = try await http.send(request, bodyData) - guard 200 ..< 300 ~= response.statusCode else { - throw handleError(response: response) + guard 200..<300 ~= response.status.code else { + throw handleError(data: data, response: response) } - return response + return (data, response) } @discardableResult - func authorizedExecute(_ request: Helpers.HTTPRequest) async throws -> Helpers.HTTPResponse { + func authorizedExecute( + for request: HTTPRequest, + from bodyData: Data? + ) async throws -> (Data, HTTPResponse) { var sessionManager: SessionManager { Dependencies[clientID].sessionManager } @@ -58,31 +67,36 @@ struct APIClient: Sendable { let session = try await sessionManager.session() var request = request - request.headers[.authorization] = "Bearer \(session.accessToken)" + request.headerFields[.authorization] = "Bearer \(session.accessToken)" - return try await execute(request) + return try await execute(for: request, from: bodyData) } - func handleError(response: Helpers.HTTPResponse) -> AuthError { - guard let error = try? response.decoded( - as: _RawAPIErrorResponse.self, - decoder: configuration.decoder - ) else { + func handleError(data: Data, response: HTTPResponse) -> AuthError { + guard + let error = try? configuration.decoder.decode( + _RawAPIErrorResponse.self, + from: data + ) + else { return .api( message: "Unexpected error", errorCode: .unexpectedFailure, - underlyingData: response.data, - underlyingResponse: response.underlyingResponse + data: data, + response: response ) } let responseAPIVersion = parseResponseAPIVersion(response) - let errorCode: ErrorCode? = if let responseAPIVersion, responseAPIVersion >= apiVersions[._20240101]!.timestamp, let code = error.code { - ErrorCode(code) - } else { - error.errorCode - } + let errorCode: ErrorCode? = + if let responseAPIVersion, responseAPIVersion >= apiVersions[._20240101]!.timestamp, + let code = error.code + { + ErrorCode(code) + } else { + error.errorCode + } if errorCode == nil, let weakPassword = error.weakPassword { return .weakPassword( @@ -100,14 +114,14 @@ struct APIClient: Sendable { return .api( message: error._getErrorMessage(), errorCode: errorCode ?? .unknown, - underlyingData: response.data, - underlyingResponse: response.underlyingResponse + data: data, + response: response ) } } - private func parseResponseAPIVersion(_ response: Helpers.HTTPResponse) -> Date? { - guard let apiVersion = response.headers[.apiVersionHeaderName] else { return nil } + private func parseResponseAPIVersion(_ response: HTTPResponse) -> Date? { + guard let apiVersion = response.headerFields[.apiVersionHeaderName] else { return nil } let formatter = ISO8601DateFormatter() formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] diff --git a/Sources/Auth/Internal/Contants.swift b/Sources/Auth/Internal/Contants.swift index e504a2ee..7b99330f 100644 --- a/Sources/Auth/Internal/Contants.swift +++ b/Sources/Auth/Internal/Contants.swift @@ -21,7 +21,7 @@ extension HTTPField.Name { } let apiVersions: [APIVersion.Name: APIVersion] = [ - ._20240101: ._20240101, + ._20240101: ._20240101 ] struct APIVersion { diff --git a/Sources/Auth/Internal/EventEmitter.swift b/Sources/Auth/Internal/EventEmitter.swift index 53b66b76..3fa7ffff 100644 --- a/Sources/Auth/Internal/EventEmitter.swift +++ b/Sources/Auth/Internal/EventEmitter.swift @@ -3,7 +3,10 @@ import Foundation import Helpers struct AuthStateChangeEventEmitter { - var emitter = EventEmitter<(AuthChangeEvent, Session?)?>(initialEvent: nil, emitsLastEventWhenAttaching: false) + var emitter = EventEmitter<(AuthChangeEvent, Session?)?>( + initialEvent: nil, + emitsLastEventWhenAttaching: false + ) var logger: (any SupabaseLogger)? func attach(_ listener: @escaping AuthStateChangeListener) -> ObservationToken { diff --git a/Sources/Auth/Internal/FixedWidthInteger+Random.swift b/Sources/Auth/Internal/FixedWidthInteger+Random.swift index c16dec4f..20973d55 100644 --- a/Sources/Auth/Internal/FixedWidthInteger+Random.swift +++ b/Sources/Auth/Internal/FixedWidthInteger+Random.swift @@ -15,13 +15,13 @@ extension FixedWidthInteger { extension Array where Element: FixedWidthInteger { static func random(count: Int) -> [Element] { var array: [Element] = .init(repeating: 0, count: count) - (0 ..< count).forEach { array[$0] = Element.random() } + (0.. [Element] { var array: [Element] = .init(repeating: 0, count: count) - (0 ..< count).forEach { array[$0] = Element.random(using: &generator) } + (0.. ( - Data, URLResponse - ) + public typealias FetchHandler = @Sendable ( + _ request: HTTPRequest, + _ bodyData: Data? + ) async throws -> (Data, HTTPResponse) /// The base URL for the functions. let url: URL @@ -45,10 +47,16 @@ public final class FunctionsClient: Sendable { @_disfavoredOverload public convenience init( url: URL, - headers: [String: String] = [:], + headers: HTTPFields = [:], region: String? = nil, logger: (any SupabaseLogger)? = nil, - fetch: @escaping FetchHandler = { try await URLSession.shared.data(for: $0) } + fetch: @escaping FetchHandler = { request, bodyData in + if let bodyData { + try await URLSession.shared.upload(for: request, from: bodyData) + } else { + try await URLSession.shared.data(for: request) + } + } ) { var interceptors: [any HTTPClientInterceptor] = [] if let logger { @@ -62,7 +70,7 @@ public final class FunctionsClient: Sendable { init( url: URL, - headers: [String: String], + headers: HTTPFields, region: String?, http: any HTTPClientType ) { @@ -71,7 +79,7 @@ public final class FunctionsClient: Sendable { self.http = http mutableState.withValue { - $0.headers = HTTPFields(headers) + $0.headers = headers if $0.headers[.xClientInfo] == nil { $0.headers[.xClientInfo] = "functions-swift/\(version)" } @@ -88,10 +96,16 @@ public final class FunctionsClient: Sendable { /// - fetch: The fetch handler used to make requests. (Default: URLSession.shared.data(for:)) public convenience init( url: URL, - headers: [String: String] = [:], + headers: HTTPFields = [:], region: FunctionRegion? = nil, logger: (any SupabaseLogger)? = nil, - fetch: @escaping FetchHandler = { try await URLSession.shared.data(for: $0) } + fetch: @escaping FetchHandler = { request, bodyData in + if let bodyData { + try await URLSession.shared.upload(for: request, from: bodyData) + } else { + try await URLSession.shared.data(for: request) + } + } ) { self.init(url: url, headers: headers, region: region?.rawValue, logger: logger, fetch: fetch) } @@ -120,12 +134,12 @@ public final class FunctionsClient: Sendable { public func invoke( _ functionName: String, options: FunctionInvokeOptions = .init(), - decode: (Data, HTTPURLResponse) throws -> Response + decode: (Data, HTTPResponse) throws -> Response ) async throws -> Response { - let response = try await rawInvoke( + let (data, response) = try await rawInvoke( functionName: functionName, invokeOptions: options ) - return try decode(response.data, response.underlyingResponse) + return try decode(data, response) } /// Invokes a function and decodes the response as a specific type. @@ -160,20 +174,20 @@ public final class FunctionsClient: Sendable { private func rawInvoke( functionName: String, invokeOptions: FunctionInvokeOptions - ) async throws -> Helpers.HTTPResponse { - let request = buildRequest(functionName: functionName, options: invokeOptions) - let response = try await http.send(request) + ) async throws -> (Data, HTTPResponse) { + let (request, bodyData) = buildRequest(functionName: functionName, options: invokeOptions) + let (data, response) = try await http.send(request, bodyData) - guard 200 ..< 300 ~= response.statusCode else { - throw FunctionsError.httpError(code: response.statusCode, data: response.data) + guard 200..<300 ~= response.status.code else { + throw FunctionsError.httpError(code: response.status.code, data: data) } - let isRelayError = response.headers[.xRelayError] == "true" + let isRelayError = response.headerFields[.xRelayError] == "true" if isRelayError { throw FunctionsError.relayError } - return response + return (data, response) } /// Invokes a function with streamed response. @@ -196,7 +210,9 @@ public final class FunctionsClient: Sendable { let session = URLSession(configuration: .default, delegate: delegate, delegateQueue: nil) - let urlRequest = buildRequest(functionName: functionName, options: invokeOptions).urlRequest + let (request, bodyData) = buildRequest(functionName: functionName, options: invokeOptions) + var urlRequest = URLRequest(httpRequest: request)! + urlRequest.httpBody = bodyData let task = session.dataTask(with: urlRequest) task.resume() @@ -211,20 +227,23 @@ public final class FunctionsClient: Sendable { return stream } - private func buildRequest(functionName: String, options: FunctionInvokeOptions) -> Helpers.HTTPRequest { + private func buildRequest( + functionName: String, + options: FunctionInvokeOptions + ) -> (HTTPRequest, Data?) { var request = HTTPRequest( - url: url.appendingPathComponent(functionName), method: options.httpMethod ?? .post, - query: options.query, - headers: mutableState.headers.merging(with: options.headers), - body: options.body + url: url + .appendingPathComponent(functionName) + .appendingQueryItems(options.query), + headerFields: mutableState.headers.merging(with: options.headers) ) if let region = options.region ?? region { - request.headers[.xRegion] = region + request.headerFields[.xRegion] = region } - return request + return (request, options.body) } } @@ -243,13 +262,18 @@ final class StreamResponseDelegate: NSObject, URLSessionDataDelegate, Sendable { continuation.finish(throwing: error) } - func urlSession(_: URLSession, dataTask _: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) { + func urlSession( + _: URLSession, + dataTask _: URLSessionDataTask, + didReceive response: URLResponse, + completionHandler: @escaping (URLSession.ResponseDisposition) -> Void + ) { guard let httpResponse = response as? HTTPURLResponse else { continuation.finish(throwing: URLError(.badServerResponse)) return } - guard 200 ..< 300 ~= httpResponse.statusCode else { + guard 200..<300 ~= httpResponse.statusCode else { let error = FunctionsError.httpError(code: httpResponse.statusCode, data: Data()) continuation.finish(throwing: error) return diff --git a/Sources/Functions/Types.swift b/Sources/Functions/Types.swift index e409c665..afa734fc 100644 --- a/Sources/Functions/Types.swift +++ b/Sources/Functions/Types.swift @@ -1,6 +1,6 @@ import Foundation -import Helpers import HTTPTypes +import Helpers /// An error type representing various errors that can occur while invoking functions. public enum FunctionsError: Error, LocalizedError { @@ -45,7 +45,7 @@ public struct FunctionInvokeOptions: Sendable { public init( method: Method? = nil, query: [URLQueryItem] = [], - headers: [String: String] = [:], + headers: HTTPFields = [:], region: String? = nil, body: some Encodable ) { @@ -65,7 +65,7 @@ public struct FunctionInvokeOptions: Sendable { } self.method = method - self.headers = defaultHeaders.merging(with: HTTPFields(headers)) + self.headers = defaultHeaders.merging(with: headers) self.region = region self.query = query } @@ -81,11 +81,11 @@ public struct FunctionInvokeOptions: Sendable { public init( method: Method? = nil, query: [URLQueryItem] = [], - headers: [String: String] = [:], + headers: HTTPFields = [:], region: String? = nil ) { self.method = method - self.headers = HTTPFields(headers) + self.headers = headers self.region = region self.query = query body = nil @@ -144,7 +144,7 @@ extension FunctionInvokeOptions { /// - body: The body data to be sent with the function invocation. (Default: nil) public init( method: Method? = nil, - headers: [String: String] = [:], + headers: HTTPFields = [:], region: FunctionRegion? = nil, body: some Encodable ) { @@ -164,7 +164,7 @@ extension FunctionInvokeOptions { /// - region: The Region to invoke the function in. public init( method: Method? = nil, - headers: [String: String] = [:], + headers: HTTPFields = [:], region: FunctionRegion? = nil ) { self.init(method: method, headers: headers, region: region?.rawValue) diff --git a/Sources/Helpers/FoundationExtensions.swift b/Sources/Helpers/FoundationExtensions.swift index 00b1ba83..e39a0789 100644 --- a/Sources/Helpers/FoundationExtensions.swift +++ b/Sources/Helpers/FoundationExtensions.swift @@ -79,7 +79,7 @@ extension CharacterSet { /// 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 sbURLQueryAllowed: CharacterSet = { - let generalDelimitersToEncode = ":#[]@" // does not include "?" or "/" due to RFC 3986 - Section 3.4 + let generalDelimitersToEncode = ":#[]@" // does not include "?" or "/" due to RFC 3986 - Section 3.4 let subDelimitersToEncode = "!$&'()*+,;=" let encodableDelimiters = CharacterSet(charactersIn: "\(generalDelimitersToEncode)\(subDelimitersToEncode)") diff --git a/Sources/Helpers/HTTP/HTTPClient.swift b/Sources/Helpers/HTTP/HTTPClient.swift index 16446303..09e83170 100644 --- a/Sources/Helpers/HTTP/HTTPClient.swift +++ b/Sources/Helpers/HTTP/HTTPClient.swift @@ -6,51 +6,61 @@ // import Foundation +import HTTPTypes #if canImport(FoundationNetworking) import FoundationNetworking #endif package protocol HTTPClientType: Sendable { - func send(_ request: HTTPRequest) async throws -> HTTPResponse + func send(_ request: HTTPRequest, _ bodyData: Data?) async throws -> (Data, HTTPResponse) } package actor HTTPClient: HTTPClientType { - let fetch: @Sendable (URLRequest) async throws -> (Data, URLResponse) + let fetch: @Sendable (HTTPRequest, Data?) async throws -> (Data, HTTPResponse) let interceptors: [any HTTPClientInterceptor] package init( - fetch: @escaping @Sendable (URLRequest) async throws -> (Data, URLResponse), + fetch: @escaping @Sendable (HTTPRequest, Data?) async throws -> (Data, HTTPResponse), interceptors: [any HTTPClientInterceptor] ) { self.fetch = fetch self.interceptors = interceptors } - package func send(_ request: HTTPRequest) async throws -> HTTPResponse { - var next: @Sendable (HTTPRequest) async throws -> HTTPResponse = { _request in - let urlRequest = _request.urlRequest - let (data, response) = try await self.fetch(urlRequest) - guard let httpURLResponse = response as? HTTPURLResponse else { - throw URLError(.badServerResponse) - } - return HTTPResponse(data: data, response: httpURLResponse) + package func send( + _ request: HTTPRequest, + _ bodyData: Data? + ) async throws -> (Data, HTTPResponse) { + var next: @Sendable (HTTPRequest, Data?) async throws -> (Data, HTTPResponse) = { + return try await self.fetch($0, $1) } for interceptor in interceptors.reversed() { let tmp = next next = { - try await interceptor.intercept($0, next: tmp) + try await interceptor.intercept($0, $1, next: tmp) } } - return try await next(request) + return try await next(request, bodyData) } } package protocol HTTPClientInterceptor: Sendable { func intercept( _ request: HTTPRequest, - next: @Sendable (HTTPRequest) async throws -> HTTPResponse - ) async throws -> HTTPResponse + _ bodyData: Data?, + next: @Sendable (HTTPRequest, Data?) async throws -> (Data, HTTPResponse) + ) async throws -> (Data, HTTPResponse) +} + +extension [URLQueryItem] { + package mutating func appendOrUpdate(_ queryItem: URLQueryItem) { + if let index = self.firstIndex(where: { $0.name == queryItem.name }) { + self[index] = queryItem + } else { + self.append(queryItem) + } + } } diff --git a/Sources/Helpers/HTTP/HTTPFields.swift b/Sources/Helpers/HTTP/HTTPFields.swift index a193533f..141e1927 100644 --- a/Sources/Helpers/HTTP/HTTPFields.swift +++ b/Sources/Helpers/HTTP/HTTPFields.swift @@ -1,27 +1,23 @@ import HTTPTypes -package extension HTTPFields { - init(_ dictionary: [String: String]) { - self.init(dictionary.map { .init(name: .init($0.key)!, value: $0.value) }) - } - - var dictionary: [String: String] { +extension HTTPFields { + package var dictionary: [String: String] { let keyValues = self.map { ($0.name.rawName, $0.value) } - + return .init(keyValues, uniquingKeysWith: { $1 }) } - - mutating func merge(with other: Self) { + + package mutating func merge(with other: Self) { for field in other { self[field.name] = field.value } } - - func merging(with other: Self) -> Self { + + package func merging(with other: Self) -> Self { var copy = self - + for field in other { copy[field.name] = field.value } @@ -30,8 +26,9 @@ package extension HTTPFields { } } -package extension HTTPField.Name { - static let xClientInfo = HTTPField.Name("X-Client-Info")! - static let xRegion = HTTPField.Name("x-region")! - static let xRelayError = HTTPField.Name("x-relay-error")! +extension HTTPField.Name { + package static let xClientInfo = HTTPField.Name("X-Client-Info")! + package static let xRegion = HTTPField.Name("x-region")! + package static let xRelayError = HTTPField.Name("x-relay-error")! + package static let apiKey = HTTPField.Name("apiKey")! } diff --git a/Sources/Helpers/HTTP/HTTPRequest.swift b/Sources/Helpers/HTTP/HTTPRequest.swift deleted file mode 100644 index 47e8adc7..00000000 --- a/Sources/Helpers/HTTP/HTTPRequest.swift +++ /dev/null @@ -1,69 +0,0 @@ -// -// HTTPRequest.swift -// -// -// Created by Guilherme Souza on 23/04/24. -// - -import Foundation -import HTTPTypes - -#if canImport(FoundationNetworking) - import FoundationNetworking -#endif - -package struct HTTPRequest: Sendable { - package var url: URL - package var method: HTTPTypes.HTTPRequest.Method - package var query: [URLQueryItem] - package var headers: HTTPFields - package var body: Data? - - package init( - url: URL, - method: HTTPTypes.HTTPRequest.Method, - query: [URLQueryItem] = [], - headers: HTTPFields = [:], - body: Data? = nil - ) { - self.url = url - self.method = method - self.query = query - self.headers = headers - self.body = body - } - - package init?( - urlString: String, - method: HTTPTypes.HTTPRequest.Method, - query: [URLQueryItem] = [], - headers: HTTPFields = [:], - body: Data? - ) { - guard let url = URL(string: urlString) else { return nil } - self.init(url: url, method: method, query: query, headers: headers, body: body) - } - - package var urlRequest: URLRequest { - var urlRequest = URLRequest(url: query.isEmpty ? url : url.appendingQueryItems(query)) - urlRequest.httpMethod = method.rawValue - urlRequest.allHTTPHeaderFields = .init(headers.map { ($0.name.rawName, $0.value) }) { $1 } - urlRequest.httpBody = body - - if urlRequest.httpBody != nil, urlRequest.value(forHTTPHeaderField: "Content-Type") == nil { - urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type") - } - - return urlRequest - } -} - -extension [URLQueryItem] { - package mutating func appendOrUpdate(_ queryItem: URLQueryItem) { - if let index = firstIndex(where: { $0.name == queryItem.name }) { - self[index] = queryItem - } else { - self.append(queryItem) - } - } -} diff --git a/Sources/Helpers/HTTP/HTTPResponse.swift b/Sources/Helpers/HTTP/HTTPResponse.swift deleted file mode 100644 index bc8a7271..00000000 --- a/Sources/Helpers/HTTP/HTTPResponse.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// HTTPResponse.swift -// -// -// Created by Guilherme Souza on 30/04/24. -// - -import Foundation -import HTTPTypes - -#if canImport(FoundationNetworking) - import FoundationNetworking -#endif - -package struct HTTPResponse: Sendable { - package let data: Data - package let headers: HTTPFields - package let statusCode: Int - - package let underlyingResponse: HTTPURLResponse - - package init(data: Data, response: HTTPURLResponse) { - self.data = data - headers = HTTPFields(response.allHeaderFields as? [String: String] ?? [:]) - statusCode = response.statusCode - underlyingResponse = response - } -} - -extension HTTPResponse { - package func decoded(as _: T.Type = T.self, decoder: JSONDecoder = JSONDecoder()) throws -> T { - try decoder.decode(T.self, from: data) - } -} diff --git a/Sources/Helpers/HTTP/LoggerInterceptor.swift b/Sources/Helpers/HTTP/LoggerInterceptor.swift index e5881953..226493be 100644 --- a/Sources/Helpers/HTTP/LoggerInterceptor.swift +++ b/Sources/Helpers/HTTP/LoggerInterceptor.swift @@ -6,6 +6,8 @@ // import Foundation +import HTTPTypes +import HTTPTypesFoundation package struct LoggerInterceptor: HTTPClientInterceptor { let logger: any SupabaseLogger @@ -16,30 +18,29 @@ package struct LoggerInterceptor: HTTPClientInterceptor { package func intercept( _ request: HTTPRequest, - next: @Sendable (HTTPRequest) async throws -> HTTPResponse - ) async throws -> HTTPResponse { + _ bodyData: Data?, + next: @Sendable (HTTPRequest, Data?) async throws -> (Data, HTTPResponse) + ) async throws -> (Data, HTTPResponse) { let id = UUID().uuidString return try await SupabaseLoggerTaskLocal.$additionalContext.withValue(merging: ["requestID": .string(id)]) { - let urlRequest = request.urlRequest - logger.verbose( """ - Request: \(urlRequest.httpMethod ?? "") \(urlRequest.url?.absoluteString.removingPercentEncoding ?? "") - Body: \(stringfy(request.body)) + Request: \(request.method.rawValue) \(request.url?.absoluteString.removingPercentEncoding ?? "") + Body: \(stringfy(bodyData)) """ ) do { - let response = try await next(request) + let (data, response) = try await next(request, bodyData) logger.verbose( """ - Response: Status code: \(response.statusCode) Content-Length: \( - response.underlyingResponse.expectedContentLength + Response: Status code: \(response.status.code) Content-Length: \( + response.headerFields[.contentLength] ?? "" ) - Body: \(stringfy(response.data)) + Body: \(stringfy(data)) """ ) - return response + return (data, response) } catch { logger.error("Response: Failure \(error)") throw error diff --git a/Sources/Helpers/HTTP/RetryRequestInterceptor.swift b/Sources/Helpers/HTTP/RetryRequestInterceptor.swift index 68178206..d053dce4 100644 --- a/Sources/Helpers/HTTP/RetryRequestInterceptor.swift +++ b/Sources/Helpers/HTTP/RetryRequestInterceptor.swift @@ -80,15 +80,20 @@ package actor RetryRequestInterceptor: HTTPClientInterceptor { /// - Returns: The HTTP response obtained after retrying. package func intercept( _ request: HTTPRequest, - next: @Sendable (HTTPRequest) async throws -> HTTPResponse - ) async throws -> HTTPResponse { - try await retry(request, retryCount: 1, next: next) + _ bodyData: Data?, + next: @Sendable (HTTPRequest, Data?) async throws -> (Data, HTTPResponse) + ) async throws -> (Data, HTTPResponse) { + try await retry(request, bodyData, retryCount: 1, next: next) } - private func shouldRetry(request: HTTPRequest, result: Result) -> Bool { + private func shouldRetry( + request: HTTPRequest, + bodyData: Data?, + result: Result<(Data, HTTPResponse), any Error> + ) -> Bool { guard retryableHTTPMethods.contains(request.method) else { return false } - if let statusCode = result.value?.statusCode, retryableHTTPStatusCodes.contains(statusCode) { + if let statusCode = result.value?.1.status.code, retryableHTTPStatusCodes.contains(statusCode) { return true } @@ -101,19 +106,20 @@ package actor RetryRequestInterceptor: HTTPClientInterceptor { private func retry( _ request: HTTPRequest, + _ bodyData: Data?, retryCount: Int, - next: @Sendable (HTTPRequest) async throws -> HTTPResponse - ) async throws -> HTTPResponse { - let result: Result + next: @Sendable (HTTPRequest, Data?) async throws -> (Data, HTTPResponse) + ) async throws -> (Data, HTTPResponse) { + let result: Result<(Data, HTTPResponse), any Error> do { - let response = try await next(request) - result = .success(response) + let (data, response) = try await next(request, bodyData) + result = .success((data, response)) } catch { result = .failure(error) } - if retryCount < retryLimit, shouldRetry(request: request, result: result) { + if retryCount < retryLimit, shouldRetry(request: request, bodyData: bodyData, result: result) { let retryDelay = pow( Double(exponentialBackoffBase), Double(retryCount) @@ -123,7 +129,7 @@ package actor RetryRequestInterceptor: HTTPClientInterceptor { try? await Task.sleep(nanoseconds: NSEC_PER_SEC * nanoseconds) if !Task.isCancelled { - return try await retry(request, retryCount: retryCount + 1, next: next) + return try await retry(request, bodyData, retryCount: retryCount + 1, next: next) } } diff --git a/Sources/Helpers/SharedModels/HTTPError.swift b/Sources/Helpers/SharedModels/HTTPError.swift index 6b36f59b..ed8c2d1d 100644 --- a/Sources/Helpers/SharedModels/HTTPError.swift +++ b/Sources/Helpers/SharedModels/HTTPError.swift @@ -6,6 +6,7 @@ // import Foundation +import HTTPTypes #if canImport(FoundationNetworking) import FoundationNetworking @@ -16,9 +17,9 @@ import Foundation /// Contains both the `Data` and `HTTPURLResponse` which you can use to extract more information about it. public struct HTTPError: Error, Sendable { public let data: Data - public let response: HTTPURLResponse + public let response: HTTPResponse - public init(data: Data, response: HTTPURLResponse) { + public init(data: Data, response: HTTPResponse) { self.data = data self.response = response } @@ -26,7 +27,7 @@ public struct HTTPError: Error, Sendable { extension HTTPError: LocalizedError { public var errorDescription: String? { - var message = "Status Code: \(response.statusCode)" + var message = "Status Code: \(response.status.code)" if let body = String(data: data, encoding: .utf8) { message += " Body: \(body)" } diff --git a/Sources/PostgREST/Defaults.swift b/Sources/PostgREST/Defaults.swift index 238047a2..1084a2f6 100644 --- a/Sources/PostgREST/Defaults.swift +++ b/Sources/PostgREST/Defaults.swift @@ -7,6 +7,7 @@ import ConcurrencyExtras import Foundation +import HTTPTypes import Helpers let version = Helpers.version @@ -44,7 +45,7 @@ extension PostgrestClient.Configuration { return encoder }() - public static let defaultHeaders: [String: String] = [ - "X-Client-Info": "postgrest-swift/\(version)", + public static let defaultHeaders: HTTPFields = [ + .xClientInfo: "postgrest-swift/\(version)" ] } diff --git a/Sources/PostgREST/Deprecated.swift b/Sources/PostgREST/Deprecated.swift index d2ffb46b..f17c6b2e 100644 --- a/Sources/PostgREST/Deprecated.swift +++ b/Sources/PostgREST/Deprecated.swift @@ -6,6 +6,8 @@ // import Foundation +import HTTPTypes +import HTTPTypesFoundation #if canImport(FoundationNetworking) import FoundationNetworking @@ -22,14 +24,20 @@ extension PostgrestClient.Configuration { /// - decoder: The JSONDecoder to use for decoding. @available( *, - deprecated, - message: "Replace usages of this initializer with new init(url:schema:headers:logger:fetch:encoder:decoder:)" + deprecated, + message: "Replace usages of this initializer with new init(url:schema:headers:logger:fetch:encoder:decoder:)" ) public init( url: URL, schema: String? = nil, - headers: [String: String] = [:], - fetch: @escaping PostgrestClient.FetchHandler = { try await URLSession.shared.data(for: $0) }, + headers: HTTPFields = [:], + fetch: @escaping PostgrestClient.FetchHandler = { request, bodyData in + if let bodyData { + try await URLSession.shared.upload(for: request, from: bodyData) + } else { + try await URLSession.shared.data(for: request) + } + }, encoder: JSONEncoder = PostgrestClient.Configuration.jsonEncoder, decoder: JSONDecoder = PostgrestClient.Configuration.jsonDecoder ) { @@ -62,8 +70,14 @@ extension PostgrestClient { public convenience init( url: URL, schema: String? = nil, - headers: [String: String] = [:], - fetch: @escaping FetchHandler = { try await URLSession.shared.data(for: $0) }, + headers: HTTPFields = [:], + fetch: @escaping FetchHandler = { request, bodyData in + if let bodyData { + try await URLSession.shared.upload(for: request, from: bodyData) + } else { + try await URLSession.shared.data(for: request) + } + }, encoder: JSONEncoder = PostgrestClient.Configuration.jsonEncoder, decoder: JSONDecoder = PostgrestClient.Configuration.jsonDecoder ) { diff --git a/Sources/PostgREST/PostgrestBuilder.swift b/Sources/PostgREST/PostgrestBuilder.swift index ce02e3c2..3726a97a 100644 --- a/Sources/PostgREST/PostgrestBuilder.swift +++ b/Sources/PostgREST/PostgrestBuilder.swift @@ -1,7 +1,7 @@ import ConcurrencyExtras import Foundation -import Helpers import HTTPTypes +import Helpers #if canImport(FoundationNetworking) import FoundationNetworking @@ -14,7 +14,8 @@ public class PostgrestBuilder: @unchecked Sendable { let http: any HTTPClientType struct MutableState { - var request: Helpers.HTTPRequest + var request: HTTPRequest + var bodyData: Data? /// The options for fetching data from the PostgREST server. var fetchOptions: FetchOptions @@ -24,7 +25,8 @@ public class PostgrestBuilder: @unchecked Sendable { init( configuration: PostgrestClient.Configuration, - request: Helpers.HTTPRequest + request: HTTPRequest, + bodyData: Data? ) { self.configuration = configuration @@ -38,6 +40,7 @@ public class PostgrestBuilder: @unchecked Sendable { mutableState = LockIsolated( MutableState( request: request, + bodyData: bodyData, fetchOptions: FetchOptions() ) ) @@ -46,7 +49,8 @@ public class PostgrestBuilder: @unchecked Sendable { convenience init(_ other: PostgrestBuilder) { self.init( configuration: other.configuration, - request: other.mutableState.value.request + request: other.mutableState.value.request, + bodyData: other.mutableState.value.bodyData ) } @@ -60,7 +64,7 @@ public class PostgrestBuilder: @unchecked Sendable { @discardableResult internal func setHeader(name: HTTPField.Name, value: String) -> Self { mutableState.withValue { - $0.request.headers[name] = value + $0.request.headerFields[name] = value } return self } @@ -106,41 +110,41 @@ public class PostgrestBuilder: @unchecked Sendable { } if let count = $0.fetchOptions.count { - if let prefer = $0.request.headers[.prefer] { - $0.request.headers[.prefer] = "\(prefer),count=\(count.rawValue)" + if let prefer = $0.request.headerFields[.prefer] { + $0.request.headerFields[.prefer] = "\(prefer),count=\(count.rawValue)" } else { - $0.request.headers[.prefer] = "count=\(count.rawValue)" + $0.request.headerFields[.prefer] = "count=\(count.rawValue)" } } - if $0.request.headers[.accept] == nil { - $0.request.headers[.accept] = "application/json" + if $0.request.headerFields[.accept] == nil { + $0.request.headerFields[.accept] = "application/json" } - $0.request.headers[.contentType] = "application/json" + $0.request.headerFields[.contentType] = "application/json" if let schema = configuration.schema { if $0.request.method == .get || $0.request.method == .head { - $0.request.headers[.acceptProfile] = schema + $0.request.headerFields[.acceptProfile] = schema } else { - $0.request.headers[.contentProfile] = schema + $0.request.headerFields[.contentProfile] = schema } } return $0.request } - let response = try await http.send(request) + let (data, response) = try await http.send(request, nil) - guard 200 ..< 300 ~= response.statusCode else { - if let error = try? configuration.decoder.decode(PostgrestError.self, from: response.data) { + guard 200..<300 ~= response.status.code else { + if let error = try? configuration.decoder.decode(PostgrestError.self, from: data) { throw error } - throw HTTPError(data: response.data, response: response.underlyingResponse) + throw HTTPError(data: data, response: response) } - let value = try decode(response.data) - return PostgrestResponse(data: response.data, response: response.underlyingResponse, value: value) + let value = try decode(data) + return PostgrestResponse(data: data, response: response, value: value) } } diff --git a/Sources/PostgREST/PostgrestClient.swift b/Sources/PostgREST/PostgrestClient.swift index 9c651c50..701d07b8 100644 --- a/Sources/PostgREST/PostgrestClient.swift +++ b/Sources/PostgREST/PostgrestClient.swift @@ -1,6 +1,7 @@ import ConcurrencyExtras import Foundation import HTTPTypes +import HTTPTypesFoundation import Helpers public typealias PostgrestError = Helpers.PostgrestError @@ -13,15 +14,16 @@ public typealias AnyJSON = Helpers.AnyJSON /// PostgREST client. public final class PostgrestClient: Sendable { - public typealias FetchHandler = @Sendable (_ request: URLRequest) async throws -> ( - Data, URLResponse - ) + public typealias FetchHandler = @Sendable ( + _ request: HTTPRequest, + _ bodyData: Data? + ) async throws -> (Data, HTTPResponse) /// The configuration struct for the PostgREST client. public struct Configuration: Sendable { public var url: URL public var schema: String? - public var headers: [String: String] + public var headers: HTTPFields public var fetch: FetchHandler public var encoder: JSONEncoder public var decoder: JSONDecoder @@ -40,9 +42,15 @@ public final class PostgrestClient: Sendable { public init( url: URL, schema: String? = nil, - headers: [String: String] = [:], + headers: HTTPFields = [:], logger: (any SupabaseLogger)? = nil, - fetch: @escaping FetchHandler = { try await URLSession.shared.data(for: $0) }, + fetch: @escaping FetchHandler = { request, bodyData in + if let bodyData { + return try await URLSession.shared.upload(for: request, from: bodyData) + } else { + return try await URLSession.shared.data(for: request) + } + }, encoder: JSONEncoder = PostgrestClient.Configuration.jsonEncoder, decoder: JSONDecoder = PostgrestClient.Configuration.jsonDecoder ) { @@ -64,7 +72,7 @@ public final class PostgrestClient: Sendable { public init(configuration: Configuration) { _configuration = LockIsolated(configuration) _configuration.withValue { - $0.headers.merge(Configuration.defaultHeaders) { l, _ in l } + $0.headers.merge(with: Configuration.defaultHeaders) } } @@ -80,9 +88,15 @@ public final class PostgrestClient: Sendable { public convenience init( url: URL, schema: String? = nil, - headers: [String: String] = [:], + headers: HTTPFields = [:], logger: (any SupabaseLogger)? = nil, - fetch: @escaping FetchHandler = { try await URLSession.shared.data(for: $0) }, + fetch: @escaping FetchHandler = { request, bodyData in + if let bodyData { + try await URLSession.shared.upload(for: request, from: bodyData) + } else { + try await URLSession.shared.data(for: request) + } + }, encoder: JSONEncoder = PostgrestClient.Configuration.jsonEncoder, decoder: JSONDecoder = PostgrestClient.Configuration.jsonDecoder ) { @@ -105,9 +119,9 @@ public final class PostgrestClient: Sendable { @discardableResult public func setAuth(_ token: String?) -> PostgrestClient { if let token { - _configuration.withValue { $0.headers["Authorization"] = "Bearer \(token)" } + _configuration.withValue { $0.headers[.authorization] = "Bearer \(token)" } } else { - _ = _configuration.withValue { $0.headers.removeValue(forKey: "Authorization") } + _configuration.withValue { $0.headers[.authorization] = nil } } return self } @@ -118,10 +132,11 @@ public final class PostgrestClient: Sendable { PostgrestQueryBuilder( configuration: configuration, request: .init( - url: configuration.url.appendingPathComponent(table), method: .get, - headers: HTTPFields(configuration.headers) - ) + url: configuration.url.appendingPathComponent(table), + headerFields: configuration.headers + ), + bodyData: nil ) } @@ -163,19 +178,19 @@ public final class PostgrestClient: Sendable { } var request = HTTPRequest( - url: url, method: method, - headers: HTTPFields(configuration.headers), - body: params is NoParams ? nil : body + url: url, + headerFields: configuration.headers ) if let count { - request.headers[.prefer] = "count=\(count.rawValue)" + request.headerFields[.prefer] = "count=\(count.rawValue)" } return PostgrestFilterBuilder( configuration: configuration, - request: request + request: request, + bodyData: params is NoParams ? nil : body ) } diff --git a/Sources/PostgREST/PostgrestFilterBuilder.swift b/Sources/PostgREST/PostgrestFilterBuilder.swift index 3abb64c2..b37959e7 100644 --- a/Sources/PostgREST/PostgrestFilterBuilder.swift +++ b/Sources/PostgREST/PostgrestFilterBuilder.swift @@ -17,10 +17,12 @@ public class PostgrestFilterBuilder: PostgrestTransformBuilder, @unchecked Senda let queryValue = value.queryValue mutableState.withValue { - $0.request.query.append(URLQueryItem( - name: column, - value: "not.\(op.rawValue).\(queryValue)" - )) + $0.request.url!.appendQueryItems([ + URLQueryItem( + name: column, + value: "not.\(op.rawValue).\(queryValue)" + ) + ]) } return self @@ -33,7 +35,7 @@ public class PostgrestFilterBuilder: PostgrestTransformBuilder, @unchecked Senda let key = referencedTable.map { "\($0).or" } ?? "or" let queryValue = filters.queryValue mutableState.withValue { - $0.request.query.append(URLQueryItem(name: key, value: "(\(queryValue))")) + $0.request.url!.appendQueryItems([URLQueryItem(name: key, value: "(\(queryValue))")]) } return self } @@ -51,7 +53,7 @@ public class PostgrestFilterBuilder: PostgrestTransformBuilder, @unchecked Senda ) -> PostgrestFilterBuilder { let queryValue = value.queryValue mutableState.withValue { - $0.request.query.append(URLQueryItem(name: column, value: "eq.\(queryValue)")) + $0.request.url!.appendQueryItems([URLQueryItem(name: column, value: "eq.\(queryValue)")]) } return self } @@ -67,7 +69,7 @@ public class PostgrestFilterBuilder: PostgrestTransformBuilder, @unchecked Senda ) -> PostgrestFilterBuilder { let queryValue = value.queryValue mutableState.withValue { - $0.request.query.append(URLQueryItem(name: column, value: "neq.\(queryValue)")) + $0.request.url!.appendQueryItems([URLQueryItem(name: column, value: "neq.\(queryValue)")]) } return self } @@ -83,7 +85,7 @@ public class PostgrestFilterBuilder: PostgrestTransformBuilder, @unchecked Senda ) -> PostgrestFilterBuilder { let queryValue = value.queryValue mutableState.withValue { - $0.request.query.append(URLQueryItem(name: column, value: "gt.\(queryValue)")) + $0.request.url!.appendQueryItems([URLQueryItem(name: column, value: "gt.\(queryValue)")]) } return self } @@ -99,7 +101,7 @@ public class PostgrestFilterBuilder: PostgrestTransformBuilder, @unchecked Senda ) -> PostgrestFilterBuilder { let queryValue = value.queryValue mutableState.withValue { - $0.request.query.append(URLQueryItem(name: column, value: "gte.\(queryValue)")) + $0.request.url!.appendQueryItems([URLQueryItem(name: column, value: "gte.\(queryValue)")]) } return self } @@ -115,7 +117,7 @@ public class PostgrestFilterBuilder: PostgrestTransformBuilder, @unchecked Senda ) -> PostgrestFilterBuilder { let queryValue = value.queryValue mutableState.withValue { - $0.request.query.append(URLQueryItem(name: column, value: "lt.\(queryValue)")) + $0.request.url!.appendQueryItems([URLQueryItem(name: column, value: "lt.\(queryValue)")]) } return self } @@ -131,7 +133,7 @@ public class PostgrestFilterBuilder: PostgrestTransformBuilder, @unchecked Senda ) -> PostgrestFilterBuilder { let queryValue = value.queryValue mutableState.withValue { - $0.request.query.append(URLQueryItem(name: column, value: "lte.\(queryValue)")) + $0.request.url!.appendQueryItems([URLQueryItem(name: column, value: "lte.\(queryValue)")]) } return self } @@ -147,7 +149,7 @@ public class PostgrestFilterBuilder: PostgrestTransformBuilder, @unchecked Senda ) -> PostgrestFilterBuilder { let queryValue = pattern.queryValue mutableState.withValue { - $0.request.query.append(URLQueryItem(name: column, value: "like.\(queryValue)")) + $0.request.url!.appendQueryItems([URLQueryItem(name: column, value: "like.\(queryValue)")]) } return self } @@ -170,7 +172,9 @@ public class PostgrestFilterBuilder: PostgrestTransformBuilder, @unchecked Senda ) -> PostgrestFilterBuilder { let queryValue = patterns.queryValue mutableState.withValue { - $0.request.query.append(URLQueryItem(name: column, value: "like(all).\(queryValue)")) + $0.request.url!.appendQueryItems([ + URLQueryItem(name: column, value: "like(all).\(queryValue)") + ]) } return self } @@ -185,7 +189,9 @@ public class PostgrestFilterBuilder: PostgrestTransformBuilder, @unchecked Senda ) -> PostgrestFilterBuilder { let queryValue = patterns.queryValue mutableState.withValue { - $0.request.query.append(URLQueryItem(name: column, value: "like(any).\(queryValue)")) + $0.request.url!.appendQueryItems([ + URLQueryItem(name: column, value: "like(any).\(queryValue)") + ]) } return self } @@ -201,7 +207,7 @@ public class PostgrestFilterBuilder: PostgrestTransformBuilder, @unchecked Senda ) -> PostgrestFilterBuilder { let queryValue = pattern.queryValue mutableState.withValue { - $0.request.query.append(URLQueryItem(name: column, value: "ilike.\(queryValue)")) + $0.request.url!.appendQueryItems([URLQueryItem(name: column, value: "ilike.\(queryValue)")]) } return self } @@ -224,7 +230,9 @@ public class PostgrestFilterBuilder: PostgrestTransformBuilder, @unchecked Senda ) -> PostgrestFilterBuilder { let queryValue = patterns.queryValue mutableState.withValue { - $0.request.query.append(URLQueryItem(name: column, value: "ilike(all).\(queryValue)")) + $0.request.url!.appendQueryItems([ + URLQueryItem(name: column, value: "ilike(all).\(queryValue)") + ]) } return self } @@ -239,7 +247,9 @@ public class PostgrestFilterBuilder: PostgrestTransformBuilder, @unchecked Senda ) -> PostgrestFilterBuilder { let queryValue = patterns.queryValue mutableState.withValue { - $0.request.query.append(URLQueryItem(name: column, value: "ilike(any).\(queryValue)")) + $0.request.url!.appendQueryItems([ + URLQueryItem(name: column, value: "ilike(any).\(queryValue)") + ]) } return self } @@ -258,7 +268,7 @@ public class PostgrestFilterBuilder: PostgrestTransformBuilder, @unchecked Senda ) -> PostgrestFilterBuilder { let queryValue = value.queryValue mutableState.withValue { - $0.request.query.append(URLQueryItem(name: column, value: "is.\(queryValue)")) + $0.request.url!.appendQueryItems([URLQueryItem(name: column, value: "is.\(queryValue)")]) } return self } @@ -274,12 +284,12 @@ public class PostgrestFilterBuilder: PostgrestTransformBuilder, @unchecked Senda ) -> PostgrestFilterBuilder { let queryValues = values.map(\.queryValue) mutableState.withValue { - $0.request.query.append( + $0.request.url!.appendQueryItems([ URLQueryItem( name: column, value: "in.(\(queryValues.joined(separator: ",")))" ) - ) + ]) } return self } @@ -305,7 +315,7 @@ public class PostgrestFilterBuilder: PostgrestTransformBuilder, @unchecked Senda ) -> PostgrestFilterBuilder { let queryValue = value.queryValue mutableState.withValue { - $0.request.query.append(URLQueryItem(name: column, value: "cs.\(queryValue)")) + $0.request.url!.appendQueryItems([URLQueryItem(name: column, value: "cs.\(queryValue)")]) } return self } @@ -323,7 +333,7 @@ public class PostgrestFilterBuilder: PostgrestTransformBuilder, @unchecked Senda ) -> PostgrestFilterBuilder { let queryValue = value.queryValue mutableState.withValue { - $0.request.query.append(URLQueryItem(name: column, value: "cd.\(queryValue)")) + $0.request.url!.appendQueryItems([URLQueryItem(name: column, value: "cd.\(queryValue)")]) } return self } @@ -341,7 +351,7 @@ public class PostgrestFilterBuilder: PostgrestTransformBuilder, @unchecked Senda ) -> PostgrestFilterBuilder { let queryValue = range.queryValue mutableState.withValue { - $0.request.query.append(URLQueryItem(name: column, value: "sl.\(queryValue)")) + $0.request.url!.appendQueryItems([URLQueryItem(name: column, value: "sl.\(queryValue)")]) } return self } @@ -359,7 +369,7 @@ public class PostgrestFilterBuilder: PostgrestTransformBuilder, @unchecked Senda ) -> PostgrestFilterBuilder { let queryValue = range.queryValue mutableState.withValue { - $0.request.query.append(URLQueryItem(name: column, value: "sr.\(queryValue)")) + $0.request.url!.appendQueryItems([URLQueryItem(name: column, value: "sr.\(queryValue)")]) } return self } @@ -377,7 +387,7 @@ public class PostgrestFilterBuilder: PostgrestTransformBuilder, @unchecked Senda ) -> PostgrestFilterBuilder { let queryValue = range.queryValue mutableState.withValue { - $0.request.query.append(URLQueryItem(name: column, value: "nxl.\(queryValue)")) + $0.request.url!.appendQueryItems([URLQueryItem(name: column, value: "nxl.\(queryValue)")]) } return self } @@ -395,7 +405,7 @@ public class PostgrestFilterBuilder: PostgrestTransformBuilder, @unchecked Senda ) -> PostgrestFilterBuilder { let queryValue = range.queryValue mutableState.withValue { - $0.request.query.append(URLQueryItem(name: column, value: "nxr.\(queryValue)")) + $0.request.url!.appendQueryItems([URLQueryItem(name: column, value: "nxr.\(queryValue)")]) } return self } @@ -413,7 +423,7 @@ public class PostgrestFilterBuilder: PostgrestTransformBuilder, @unchecked Senda ) -> PostgrestFilterBuilder { let queryValue = range.queryValue mutableState.withValue { - $0.request.query.append(URLQueryItem(name: column, value: "adj.\(queryValue)")) + $0.request.url!.appendQueryItems([URLQueryItem(name: column, value: "adj.\(queryValue)")]) } return self } @@ -431,7 +441,7 @@ public class PostgrestFilterBuilder: PostgrestTransformBuilder, @unchecked Senda ) -> PostgrestFilterBuilder { let queryValue = value.queryValue mutableState.withValue { - $0.request.query.append(URLQueryItem(name: column, value: "ov.\(queryValue)")) + $0.request.url!.appendQueryItems([URLQueryItem(name: column, value: "ov.\(queryValue)")]) } return self } @@ -455,11 +465,11 @@ public class PostgrestFilterBuilder: PostgrestTransformBuilder, @unchecked Senda let configPart = config.map { "(\($0))" } mutableState.withValue { - $0.request.query.append( + $0.request.url!.appendQueryItems([ URLQueryItem( name: column, value: "\(type?.rawValue ?? "")fts\(configPart ?? "").\(queryValue)" ) - ) + ]) } return self } @@ -513,10 +523,12 @@ public class PostgrestFilterBuilder: PostgrestTransformBuilder, @unchecked Senda value: String ) -> PostgrestFilterBuilder { mutableState.withValue { - $0.request.query.append(URLQueryItem( - name: column, - value: "\(`operator`).\(value)" - )) + $0.request.url!.appendQueryItems([ + URLQueryItem( + name: column, + value: "\(`operator`).\(value)" + ) + ]) } return self } @@ -530,10 +542,12 @@ public class PostgrestFilterBuilder: PostgrestTransformBuilder, @unchecked Senda let query = query.mapValues(\.queryValue) mutableState.withValue { mutableState in for (key, value) in query { - mutableState.request.query.append(URLQueryItem( - name: key, - value: "eq.\(value.queryValue)" - )) + mutableState.request.url!.appendQueryItems([ + URLQueryItem( + name: key, + value: "eq.\(value.queryValue)" + ) + ]) } } return self diff --git a/Sources/PostgREST/PostgrestQueryBuilder.swift b/Sources/PostgREST/PostgrestQueryBuilder.swift index ee231a5f..1fa9aefc 100644 --- a/Sources/PostgREST/PostgrestQueryBuilder.swift +++ b/Sources/PostgREST/PostgrestQueryBuilder.swift @@ -27,10 +27,14 @@ public final class PostgrestQueryBuilder: PostgrestBuilder, @unchecked Sendable } .joined(separator: "") - $0.request.query.appendOrUpdate(URLQueryItem(name: "select", value: cleanedColumns)) + var urlComponents = URLComponents(url: $0.request.url!, resolvingAgainstBaseURL: true)! + var queryItems = urlComponents.queryItems ?? [] + queryItems.appendOrUpdate(URLQueryItem(name: "select", value: cleanedColumns)) + urlComponents.queryItems = queryItems + $0.request.url = urlComponents.url! if let count { - $0.request.headers[.prefer] = "count=\(count.rawValue)" + $0.request.headerFields[.prefer] = "count=\(count.rawValue)" } if head { $0.request.method = .head @@ -58,25 +62,28 @@ public final class PostgrestQueryBuilder: PostgrestBuilder, @unchecked Sendable if let returning { prefersHeaders.append("return=\(returning.rawValue)") } - $0.request.body = try configuration.encoder.encode(values) + $0.bodyData = try configuration.encoder.encode(values) if let count { prefersHeaders.append("count=\(count.rawValue)") } - if let prefer = $0.request.headers[.prefer] { + if let prefer = $0.request.headerFields[.prefer] { prefersHeaders.insert(prefer, at: 0) } if !prefersHeaders.isEmpty { - $0.request.headers[.prefer] = prefersHeaders.joined(separator: ",") + $0.request.headerFields[.prefer] = prefersHeaders.joined(separator: ",") } - if let body = $0.request.body, - let jsonObject = try JSONSerialization.jsonObject(with: body) as? [[String: Any]] + if let body = $0.bodyData, + let jsonObject = try JSONSerialization.jsonObject(with: body) as? [[String: Any]] { let allKeys = jsonObject.flatMap(\.keys) let uniqueKeys = Set(allKeys).sorted() - $0.request.query.appendOrUpdate(URLQueryItem( - name: "columns", - value: uniqueKeys.joined(separator: ",") - )) + var urlComponents = URLComponents(url: $0.request.url!, resolvingAgainstBaseURL: true)! + var queryItems = urlComponents.queryItems ?? [] + queryItems.appendOrUpdate( + URLQueryItem(name: "columns", value: uniqueKeys.joined(separator: ",")) + ) + urlComponents.queryItems = queryItems + $0.request.url = urlComponents.url! } } @@ -108,28 +115,36 @@ public final class PostgrestQueryBuilder: PostgrestBuilder, @unchecked Sendable "return=\(returning.rawValue)", ] if let onConflict { - $0.request.query.appendOrUpdate(URLQueryItem(name: "on_conflict", value: onConflict)) + var urlComponents = URLComponents(url: $0.request.url!, resolvingAgainstBaseURL: true)! + var queryItems = urlComponents.queryItems ?? [] + queryItems.appendOrUpdate(URLQueryItem(name: "on_conflict", value: onConflict)) + urlComponents.queryItems = queryItems + $0.request.url = urlComponents.url! } - $0.request.body = try configuration.encoder.encode(values) + $0.bodyData = try configuration.encoder.encode(values) if let count { prefersHeaders.append("count=\(count.rawValue)") } - if let prefer = $0.request.headers[.prefer] { + if let prefer = $0.request.headerFields[.prefer] { prefersHeaders.insert(prefer, at: 0) } if !prefersHeaders.isEmpty { - $0.request.headers[.prefer] = prefersHeaders.joined(separator: ",") + $0.request.headerFields[.prefer] = prefersHeaders.joined(separator: ",") } - if let body = $0.request.body, - let jsonObject = try JSONSerialization.jsonObject(with: body) as? [[String: Any]] + if let body = $0.bodyData, + let jsonObject = try JSONSerialization.jsonObject(with: body) as? [[String: Any]] { let allKeys = jsonObject.flatMap(\.keys) let uniqueKeys = Set(allKeys).sorted() - $0.request.query.appendOrUpdate(URLQueryItem( - name: "columns", - value: uniqueKeys.joined(separator: ",") - )) + + var urlComponents = URLComponents(url: $0.request.url!, resolvingAgainstBaseURL: true)! + var queryItems = urlComponents.queryItems ?? [] + queryItems.appendOrUpdate( + URLQueryItem(name: "columns", value: uniqueKeys.joined(separator: ",")) + ) + urlComponents.queryItems = queryItems + $0.request.url = urlComponents.url! } } return PostgrestFilterBuilder(self) @@ -150,15 +165,15 @@ public final class PostgrestQueryBuilder: PostgrestBuilder, @unchecked Sendable try mutableState.withValue { $0.request.method = .patch var preferHeaders = ["return=\(returning.rawValue)"] - $0.request.body = try configuration.encoder.encode(values) + $0.bodyData = try configuration.encoder.encode(values) if let count { preferHeaders.append("count=\(count.rawValue)") } - if let prefer = $0.request.headers[.prefer] { + if let prefer = $0.request.headerFields[.prefer] { preferHeaders.insert(prefer, at: 0) } if !preferHeaders.isEmpty { - $0.request.headers[.prefer] = preferHeaders.joined(separator: ",") + $0.request.headerFields[.prefer] = preferHeaders.joined(separator: ",") } } return PostgrestFilterBuilder(self) @@ -180,11 +195,11 @@ public final class PostgrestQueryBuilder: PostgrestBuilder, @unchecked Sendable if let count { preferHeaders.append("count=\(count.rawValue)") } - if let prefer = $0.request.headers[.prefer] { + if let prefer = $0.request.headerFields[.prefer] { preferHeaders.insert(prefer, at: 0) } if !preferHeaders.isEmpty { - $0.request.headers[.prefer] = preferHeaders.joined(separator: ",") + $0.request.headerFields[.prefer] = preferHeaders.joined(separator: ",") } } return PostgrestFilterBuilder(self) diff --git a/Sources/PostgREST/PostgrestTransformBuilder.swift b/Sources/PostgREST/PostgrestTransformBuilder.swift index ead0cc0e..882778c2 100644 --- a/Sources/PostgREST/PostgrestTransformBuilder.swift +++ b/Sources/PostgREST/PostgrestTransformBuilder.swift @@ -22,9 +22,13 @@ public class PostgrestTransformBuilder: PostgrestBuilder, @unchecked Sendable { } .joined(separator: "") mutableState.withValue { - $0.request.query.appendOrUpdate(URLQueryItem(name: "select", value: cleanedColumns)) + var urlComponents = URLComponents(url: $0.request.url!, resolvingAgainstBaseURL: true)! + var queryItems = urlComponents.queryItems ?? [] + queryItems.appendOrUpdate(URLQueryItem(name: "select", value: cleanedColumns)) + urlComponents.queryItems = queryItems + $0.request.url = urlComponents.url! - if let prefer = $0.request.headers[.prefer] { + if let prefer = $0.request.headerFields[.prefer] { var components = prefer.components(separatedBy: ",") if let index = components.firstIndex(where: { $0.hasPrefix("return=") }) { @@ -33,9 +37,9 @@ public class PostgrestTransformBuilder: PostgrestBuilder, @unchecked Sendable { components.append("return=representation") } - $0.request.headers[.prefer] = components.joined(separator: ",") + $0.request.headerFields[.prefer] = components.joined(separator: ",") } else { - $0.request.headers[.prefer] = "return=representation" + $0.request.headerFields[.prefer] = "return=representation" } } return self @@ -59,20 +63,26 @@ public class PostgrestTransformBuilder: PostgrestBuilder, @unchecked Sendable { ) -> PostgrestTransformBuilder { mutableState.withValue { let key = referencedTable.map { "\($0).order" } ?? "order" - let existingOrderIndex = $0.request.query.firstIndex { $0.name == key } + var urlComponents = URLComponents(url: $0.request.url!, resolvingAgainstBaseURL: true)! + var queryItems = urlComponents.queryItems ?? [] + + let existingOrderIndex = queryItems.firstIndex { $0.name == key } let value = "\(column).\(ascending ? "asc" : "desc").\(nullsFirst ? "nullsfirst" : "nullslast")" if let existingOrderIndex, - let currentValue = $0.request.query[existingOrderIndex].value + let currentValue = queryItems[existingOrderIndex].value { - $0.request.query[existingOrderIndex] = URLQueryItem( + queryItems[existingOrderIndex] = URLQueryItem( name: key, value: "\(currentValue),\(value)" ) } else { - $0.request.query.append(URLQueryItem(name: key, value: value)) + queryItems.append(URLQueryItem(name: key, value: value)) } + + urlComponents.queryItems = queryItems + $0.request.url = urlComponents.url! } return self @@ -85,11 +95,11 @@ public class PostgrestTransformBuilder: PostgrestBuilder, @unchecked Sendable { public func limit(_ count: Int, referencedTable: String? = nil) -> PostgrestTransformBuilder { mutableState.withValue { let key = referencedTable.map { "\($0).limit" } ?? "limit" - if let index = $0.request.query.firstIndex(where: { $0.name == key }) { - $0.request.query[index] = URLQueryItem(name: key, value: "\(count)") - } else { - $0.request.query.append(URLQueryItem(name: key, value: "\(count)")) - } + var urlComponents = URLComponents(url: $0.request.url!, resolvingAgainstBaseURL: true)! + var queryItems = urlComponents.queryItems ?? [] + queryItems.appendOrUpdate(URLQueryItem(name: key, value: "\(count)")) + urlComponents.queryItems = queryItems + $0.request.url = urlComponents.url! } return self } @@ -113,24 +123,13 @@ public class PostgrestTransformBuilder: PostgrestBuilder, @unchecked Sendable { let keyLimit = referencedTable.map { "\($0).limit" } ?? "limit" mutableState.withValue { - if let index = $0.request.query.firstIndex(where: { $0.name == keyOffset }) { - $0.request.query[index] = URLQueryItem(name: keyOffset, value: "\(from)") - } else { - $0.request.query.append(URLQueryItem(name: keyOffset, value: "\(from)")) - } - + var urlComponents = URLComponents(url: $0.request.url!, resolvingAgainstBaseURL: true)! + var queryItems = urlComponents.queryItems ?? [] + queryItems.appendOrUpdate(URLQueryItem(name: keyOffset, value: "\(from)")) // Range is inclusive, so add 1 - if let index = $0.request.query.firstIndex(where: { $0.name == keyLimit }) { - $0.request.query[index] = URLQueryItem( - name: keyLimit, - value: "\(to - from + 1)" - ) - } else { - $0.request.query.append(URLQueryItem( - name: keyLimit, - value: "\(to - from + 1)" - )) - } + queryItems.appendOrUpdate(URLQueryItem(name: keyLimit, value: "\(to - from + 1)")) + urlComponents.queryItems = queryItems + $0.request.url = urlComponents.url! } return self @@ -141,7 +140,7 @@ public class PostgrestTransformBuilder: PostgrestBuilder, @unchecked Sendable { /// Query result must be one row (e.g. using `.limit(1)`), otherwise this returns an error. public func single() -> PostgrestTransformBuilder { mutableState.withValue { - $0.request.headers[.accept] = "application/vnd.pgrst.object+json" + $0.request.headerFields[.accept] = "application/vnd.pgrst.object+json" } return self } @@ -149,7 +148,7 @@ public class PostgrestTransformBuilder: PostgrestBuilder, @unchecked Sendable { /// Return `value` as a string in CSV format. public func csv() -> PostgrestTransformBuilder { mutableState.withValue { - $0.request.headers[.accept] = "text/csv" + $0.request.headerFields[.accept] = "text/csv" } return self } @@ -157,7 +156,7 @@ public class PostgrestTransformBuilder: PostgrestBuilder, @unchecked Sendable { /// Return `value` as an object in [GeoJSON](https://geojson.org) format. public func geojson() -> PostgrestTransformBuilder { mutableState.withValue { - $0.request.headers[.accept] = "application/geo+json" + $0.request.headerFields[.accept] = "application/geo+json" } return self } @@ -194,8 +193,8 @@ public class PostgrestTransformBuilder: PostgrestBuilder, @unchecked Sendable { ] .compactMap { $0 } .joined(separator: "|") - let forMediaType = $0.request.headers[.accept] ?? "application/json" - $0.request.headers[.accept] = "application/vnd.pgrst.plan+\"\(format)\"; for=\(forMediaType); options=\(options);" + let forMediaType = $0.request.headerFields[.accept] ?? "application/json" + $0.request.headerFields[.accept] = "application/vnd.pgrst.plan+\"\(format)\"; for=\(forMediaType); options=\(options);" } return self diff --git a/Sources/PostgREST/Types.swift b/Sources/PostgREST/Types.swift index b2c37888..04262e48 100644 --- a/Sources/PostgREST/Types.swift +++ b/Sources/PostgREST/Types.swift @@ -1,4 +1,5 @@ import Foundation +import HTTPTypes #if canImport(FoundationNetworking) import FoundationNetworking @@ -6,22 +7,22 @@ import Foundation public struct PostgrestResponse: Sendable { public let data: Data - public let response: HTTPURLResponse + public let response: HTTPResponse public let count: Int? public let value: T public var status: Int { - response.statusCode + response.status.code } public init( data: Data, - response: HTTPURLResponse, + response: HTTPResponse, value: T ) { var count: Int? - if let contentRange = response.value(forHTTPHeaderField: "Content-Range")?.split(separator: "/") + if let contentRange = response.headerFields[.contentRange]?.split(separator: "/") .last { count = contentRange == "*" ? nil : Int(contentRange) diff --git a/Sources/Realtime/Defaults.swift b/Sources/Realtime/Defaults.swift index e74f08bc..84f44362 100644 --- a/Sources/Realtime/Defaults.swift +++ b/Sources/Realtime/Defaults.swift @@ -57,7 +57,7 @@ public enum Defaults { public static let decode: (Data) -> Any? = { data in guard let json = - try? JSONSerialization + try? JSONSerialization .jsonObject( with: data, options: JSONSerialization.ReadingOptions() diff --git a/Sources/Realtime/Deprecated.swift b/Sources/Realtime/Deprecated.swift index 27d32f91..bbfdfe9c 100644 --- a/Sources/Realtime/Deprecated.swift +++ b/Sources/Realtime/Deprecated.swift @@ -6,6 +6,7 @@ // import Foundation +import HTTPTypes import Helpers @available(*, deprecated, renamed: "RealtimeMessage") @@ -21,7 +22,7 @@ extension RealtimeClientV2 { public struct Configuration: Sendable { var url: URL var apiKey: String - var headers: [String: String] + var headers: HTTPFields var heartbeatInterval: TimeInterval var reconnectDelay: TimeInterval var timeoutInterval: TimeInterval @@ -32,7 +33,7 @@ extension RealtimeClientV2 { public init( url: URL, apiKey: String, - headers: [String: String] = [:], + headers: HTTPFields = [:], heartbeatInterval: TimeInterval = 15, reconnectDelay: TimeInterval = 7, timeoutInterval: TimeInterval = 10, diff --git a/Sources/Realtime/PhoenixTransport.swift b/Sources/Realtime/PhoenixTransport.swift index 79c85400..3c416f66 100644 --- a/Sources/Realtime/PhoenixTransport.swift +++ b/Sources/Realtime/PhoenixTransport.swift @@ -176,8 +176,8 @@ open class URLSessionTransport: NSObject, PhoenixTransport, URLSessionWebSocketD let endpoint = url.absoluteString let wsEndpoint = endpoint - .replacingOccurrences(of: "http://", with: "ws://") - .replacingOccurrences(of: "https://", with: "wss://") + .replacingOccurrences(of: "http://", with: "ws://") + .replacingOccurrences(of: "https://", with: "wss://") // Force unwrapping should be safe here since a valid URL came in and we just // replaced the protocol. diff --git a/Sources/Realtime/Presence.swift b/Sources/Realtime/Presence.swift index 2370697f..976832f3 100644 --- a/Sources/Realtime/Presence.swift +++ b/Sources/Realtime/Presence.swift @@ -228,7 +228,7 @@ public final class Presence { joinRef = nil caller = Caller() - guard // Do not subscribe to events if they were not provided + guard // Do not subscribe to events if they were not provided let stateEvent = opts.events[.state], let diffEvent = opts.events[.diff] else { return } diff --git a/Sources/Realtime/RealtimeChannel+AsyncAwait.swift b/Sources/Realtime/RealtimeChannel+AsyncAwait.swift index c8a51c3e..bf31240f 100644 --- a/Sources/Realtime/RealtimeChannel+AsyncAwait.swift +++ b/Sources/Realtime/RealtimeChannel+AsyncAwait.swift @@ -101,7 +101,7 @@ extension RealtimeChannelV2 { return stream } - + /// Listen for `system` event. public func system() -> AsyncStream { let (stream, continuation) = AsyncStream.makeStream() @@ -125,8 +125,8 @@ extension RealtimeChannelV2 { } // Helper to work around type ambiguity in macOS 13 -fileprivate extension AsyncStream { - func compactErase() -> AsyncStream { +extension AsyncStream { + fileprivate func compactErase() -> AsyncStream { AsyncStream(compactMap { $0.wrappedAction as? T } as AsyncCompactMapSequence) } } diff --git a/Sources/Realtime/RealtimeChannel.swift b/Sources/Realtime/RealtimeChannel.swift index 380df82f..2a4b4b86 100644 --- a/Sources/Realtime/RealtimeChannel.swift +++ b/Sources/Realtime/RealtimeChannel.swift @@ -20,9 +20,9 @@ import ConcurrencyExtras import Foundation +import HTTPTypes import Helpers import Swift -import HTTPTypes /// Container class of bindings to the channel struct Binding { @@ -95,13 +95,13 @@ public struct RealtimeChannelOptions { [ "config": [ "presence": [ - "key": presenceKey ?? "", + "key": presenceKey ?? "" ], "broadcast": [ "ack": broadcastAcknowledge, "self": broadcastSelf, ], - ], + ] ] } } @@ -378,7 +378,7 @@ public class RealtimeChannel { var accessTokenPayload: Payload = [:] var config: Payload = [ - "postgres_changes": bindings.value["postgres_changes"]?.map(\.filter) ?? [], + "postgres_changes": bindings.value["postgres_changes"]?.map(\.filter) ?? [] ] config["broadcast"] = broadcast @@ -409,7 +409,7 @@ public class RealtimeChannel { let bindingsCount = clientPostgresBindings.count var newPostgresBindings: [Binding] = [] - for i in 0 ..< bindingsCount { + for i in 0.. Void)] = [] /// Ref counter for messages - var ref: UInt64 = .min // 0 (max: 18,446,744,073,709,551,615) + var ref: UInt64 = .min // 0 (max: 18,446,744,073,709,551,615) /// Timer that triggers sending new Heartbeat messages var heartbeatTimer: HeartbeatTimer? @@ -189,7 +190,7 @@ public class RealtimeClient: PhoenixTransportDelegate { @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) public convenience init( _ endPoint: String, - headers: [String: String] = [:], + headers: HTTPFields = [:], params: Payload? = nil, vsn: String = Defaults.vsn ) { @@ -205,7 +206,7 @@ public class RealtimeClient: PhoenixTransportDelegate { @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) public convenience init( _ endPoint: String, - headers: [String: String] = [:], + headers: HTTPFields = [:], paramsClosure: PayloadClosure?, vsn: String = Defaults.vsn ) { @@ -220,7 +221,7 @@ public class RealtimeClient: PhoenixTransportDelegate { public init( endPoint: String, - headers: [String: String] = [:], + headers: HTTPFields = [:], transport: @escaping ((URL) -> any PhoenixTransport), paramsClosure: PayloadClosure? = nil, vsn: String = Defaults.vsn @@ -231,11 +232,20 @@ public class RealtimeClient: PhoenixTransportDelegate { self.vsn = vsn var headers = headers - if headers["X-Client-Info"] == nil { - headers["X-Client-Info"] = "realtime-swift/\(version)" + if headers[.xClientInfo] == nil { + headers[.xClientInfo] = "realtime-swift/\(version)" } self.headers = headers - http = HTTPClient(fetch: { try await URLSession.shared.data(for: $0) }, interceptors: []) + http = HTTPClient( + fetch: { request, bodyData in + if let bodyData { + try await URLSession.shared.upload(for: request, from: bodyData) + } else { + try await URLSession.shared.data(for: request) + } + }, + interceptors: [] + ) let params = paramsClosure?() if let jwt = (params?["Authorization"] as? String)?.split(separator: " ").last { @@ -334,7 +344,7 @@ public class RealtimeClient: PhoenixTransportDelegate { // self.connection?.enabledSSLCipherSuites = enabledSSLCipherSuites // #endif - connection?.connect(with: headers) + connection?.connect(with: headers.dictionary) } /// Disconnects the socket @@ -940,7 +950,7 @@ public class RealtimeClient: PhoenixTransportDelegate { // If there is a pending heartbeat ref, then the last heartbeat was // never acknowledged by the server. Close the connection and attempt // to reconnect. - if let _ = pendingHeartbeatRef { + if pendingHeartbeatRef != nil { pendingHeartbeatRef = nil logItems( "transport", diff --git a/Sources/Realtime/RealtimeMessage.swift b/Sources/Realtime/RealtimeMessage.swift index 3feb0066..6510c726 100644 --- a/Sources/Realtime/RealtimeMessage.swift +++ b/Sources/Realtime/RealtimeMessage.swift @@ -74,8 +74,8 @@ public struct RealtimeMessage { ref = json[1] as? String ?? "" if let topic = json[2] as? String, - let event = json[3] as? String, - let payload = json[4] as? Payload + let event = json[3] as? String, + let payload = json[4] as? Payload { self.topic = topic self.event = event diff --git a/Sources/Realtime/V2/RealtimeChannelV2.swift b/Sources/Realtime/V2/RealtimeChannelV2.swift index 5e52455c..e0b98fd3 100644 --- a/Sources/Realtime/V2/RealtimeChannelV2.swift +++ b/Sources/Realtime/V2/RealtimeChannelV2.swift @@ -37,7 +37,10 @@ struct Socket: Sendable { var addChannel: @Sendable (_ channel: RealtimeChannelV2) -> Void var removeChannel: @Sendable (_ channel: RealtimeChannelV2) async -> Void var push: @Sendable (_ message: RealtimeMessageV2) async -> Void - var httpSend: @Sendable (_ request: Helpers.HTTPRequest) async throws -> Helpers.HTTPResponse + var httpSend: @Sendable ( + _ request: HTTPRequest, + _ bodyData: Data? + ) async throws -> (Data, HTTPResponse) } extension Socket { @@ -54,7 +57,7 @@ extension Socket { removeChannel: { [weak client] in await client?.removeChannel($0) }, push: { [weak client] in await client?.push($0) }, httpSend: { [weak client] in - try await client?.http.send($0) ?? .init(data: Data(), response: HTTPURLResponse()) + try await client?.http.send($0, $1) ?? (Data(), HTTPResponse(status: .ok)) } ) } @@ -220,21 +223,21 @@ public final class RealtimeChannelV2: Sendable { let task = Task { [headers] in _ = try? await socket.httpSend( HTTPRequest( - url: socket.broadcastURL(), method: .post, - headers: headers, - body: JSONEncoder().encode( - [ - "messages": [ - Message( - topic: topic, - event: event, - payload: message, - private: config.isPrivate - ) - ] + url: socket.broadcastURL(), + headerFields: headers + ), + JSONEncoder().encode( + [ + "messages": [ + Message( + topic: topic, + event: event, + payload: message, + private: config.isPrivate + ) ] - ) + ] ) ) } diff --git a/Sources/Realtime/V2/RealtimeClientV2.swift b/Sources/Realtime/V2/RealtimeClientV2.swift index 5c072c4b..656b3ff5 100644 --- a/Sources/Realtime/V2/RealtimeClientV2.swift +++ b/Sources/Realtime/V2/RealtimeClientV2.swift @@ -7,6 +7,8 @@ import ConcurrencyExtras import Foundation +import HTTPTypes +import HTTPTypesFoundation import Helpers #if canImport(FoundationNetworking) @@ -88,7 +90,13 @@ public final class RealtimeClientV2: Sendable { options: options ), http: HTTPClient( - fetch: options.fetch ?? { try await URLSession.shared.data(for: $0) }, + fetch: options.fetch ?? { request, bodyData in + if let bodyData { + return try await URLSession.shared.upload(for: request, from: bodyData) + } else { + return try await URLSession.shared.data(for: request) + } + }, interceptors: interceptors ) ) diff --git a/Sources/Realtime/V2/Types.swift b/Sources/Realtime/V2/Types.swift index 7c33c8df..e1172bc9 100644 --- a/Sources/Realtime/V2/Types.swift +++ b/Sources/Realtime/V2/Types.swift @@ -6,8 +6,8 @@ // import Foundation -import Helpers import HTTPTypes +import Helpers #if canImport(FoundationNetworking) import FoundationNetworking @@ -21,7 +21,10 @@ public struct RealtimeClientOptions: Sendable { var timeoutInterval: TimeInterval var disconnectOnSessionLoss: Bool var connectOnSubscribe: Bool - var fetch: (@Sendable (_ request: URLRequest) async throws -> (Data, URLResponse))? + var fetch: (@Sendable ( + _ request: HTTPRequest, + _ bodyData: Data? + ) async throws -> (Data, HTTPResponse))? package var logger: (any SupabaseLogger)? public static let defaultHeartbeatInterval: TimeInterval = 15 @@ -31,16 +34,19 @@ public struct RealtimeClientOptions: Sendable { public static let defaultConnectOnSubscribe: Bool = true public init( - headers: [String: String] = [:], + headers: HTTPFields = [:], heartbeatInterval: TimeInterval = Self.defaultHeartbeatInterval, reconnectDelay: TimeInterval = Self.defaultReconnectDelay, timeoutInterval: TimeInterval = Self.defaultTimeoutInterval, disconnectOnSessionLoss: Bool = Self.defaultDisconnectOnSessionLoss, connectOnSubscribe: Bool = Self.defaultConnectOnSubscribe, - fetch: (@Sendable (_ request: URLRequest) async throws -> (Data, URLResponse))? = nil, + fetch: (@Sendable ( + _ request: HTTPRequest, + _ bodyData: Data? + ) async throws -> (Data, HTTPResponse))? = nil, logger: (any SupabaseLogger)? = nil ) { - self.headers = HTTPFields(headers) + self.headers = headers self.heartbeatInterval = heartbeatInterval self.reconnectDelay = reconnectDelay self.timeoutInterval = timeoutInterval @@ -84,7 +90,3 @@ public enum RealtimeClientStatus: Sendable, CustomStringConvertible { } } } - -extension HTTPField.Name { - static let apiKey = Self("apiKey")! -} diff --git a/Sources/Storage/Deprecated.swift b/Sources/Storage/Deprecated.swift index 865328b9..db8a6dcd 100644 --- a/Sources/Storage/Deprecated.swift +++ b/Sources/Storage/Deprecated.swift @@ -6,6 +6,7 @@ // import Foundation +import HTTPTypes extension StorageClientConfiguration { @available( @@ -15,7 +16,7 @@ extension StorageClientConfiguration { ) public init( url: URL, - headers: [String: String], + headers: HTTPFields = [:], encoder: JSONEncoder = .defaultStorageEncoder, decoder: JSONDecoder = .defaultStorageDecoder, session: StorageHTTPSession = .init() diff --git a/Sources/Storage/Helpers.swift b/Sources/Storage/Helpers.swift index a8aae837..11ba43df 100644 --- a/Sources/Storage/Helpers.swift +++ b/Sources/Storage/Helpers.swift @@ -27,7 +27,7 @@ import Helpers kUTTagClassFilenameExtension, pathExtension as CFString, nil )?.takeRetainedValue(), let contentType = UTTypeCopyPreferredTagWithClass(id, kUTTagClassMIMEType)? - .takeRetainedValue() + .takeRetainedValue() { return contentType as String } @@ -43,7 +43,7 @@ import Helpers kUTTagClassFilenameExtension, pathExtension as CFString, nil )?.takeRetainedValue(), let contentType = UTTypeCopyPreferredTagWithClass(id, kUTTagClassMIMEType)? - .takeRetainedValue() + .takeRetainedValue() { return contentType as String } @@ -62,7 +62,7 @@ import Helpers kUTTagClassFilenameExtension, pathExtension as CFString, nil )?.takeRetainedValue(), let contentType = UTTypeCopyPreferredTagWithClass(id, kUTTagClassMIMEType)? - .takeRetainedValue() + .takeRetainedValue() { return contentType as String } diff --git a/Sources/Storage/StorageApi.swift b/Sources/Storage/StorageApi.swift index 838ea800..f22915a7 100644 --- a/Sources/Storage/StorageApi.swift +++ b/Sources/Storage/StorageApi.swift @@ -1,6 +1,6 @@ import Foundation -import Helpers import HTTPTypes +import Helpers #if canImport(FoundationNetworking) import FoundationNetworking @@ -13,8 +13,8 @@ public class StorageApi: @unchecked Sendable { public init(configuration: StorageClientConfiguration) { var configuration = configuration - if configuration.headers["X-Client-Info"] == nil { - configuration.headers["X-Client-Info"] = "storage-swift/\(version)" + if configuration.headers[.xClientInfo] == nil { + configuration.headers[.xClientInfo] = "storage-swift/\(version)" } self.configuration = configuration @@ -30,35 +30,37 @@ public class StorageApi: @unchecked Sendable { } @discardableResult - func execute(_ request: Helpers.HTTPRequest) async throws -> Helpers.HTTPResponse { + func execute( + for request: HTTPRequest, + from bodyData: Data? + ) async throws -> (Data, HTTPResponse) { var request = request - request.headers = HTTPFields(configuration.headers).merging(with: request.headers) + request.headerFields = configuration.headers.merging(with: request.headerFields) - let response = try await http.send(request) + let (data, response) = try await http.send(request, bodyData) - guard (200 ..< 300).contains(response.statusCode) else { + guard (200..<300).contains(response.status.code) else { if let error = try? configuration.decoder.decode( StorageError.self, - from: response.data + from: data ) { throw error } - throw HTTPError(data: response.data, response: response.underlyingResponse) + throw HTTPError(data: data, response: response) } - return response + return (data, response) } } -extension Helpers.HTTPRequest { +extension HTTPRequest { init( + method: HTTPRequest.Method, url: URL, - method: HTTPTypes.HTTPRequest.Method, - query: [URLQueryItem], - formData: MultipartFormData, options: FileOptions, - headers: HTTPFields = [:] + headers: HTTPFields = [:], + formData: MultipartFormData ) throws { var headers = headers if headers[.contentType] == nil { @@ -67,12 +69,11 @@ extension Helpers.HTTPRequest { if headers[.cacheControl] == nil { headers[.cacheControl] = "max-age=\(options.cacheControl)" } - try self.init( - url: url, + + self.init( method: method, - query: query, - headers: headers, - body: formData.encode() + url: url, + headerFields: headers ) } } diff --git a/Sources/Storage/StorageBucketApi.swift b/Sources/Storage/StorageBucketApi.swift index ddbb426a..7789ec72 100644 --- a/Sources/Storage/StorageBucketApi.swift +++ b/Sources/Storage/StorageBucketApi.swift @@ -1,4 +1,5 @@ import Foundation +import HTTPTypes import Helpers #if canImport(FoundationNetworking) @@ -9,26 +10,30 @@ import Helpers public class StorageBucketApi: StorageApi, @unchecked Sendable { /// Retrieves the details of all Storage buckets within an existing project. public func listBuckets() async throws -> [Bucket] { - try await execute( - HTTPRequest( - url: configuration.url.appendingPathComponent("bucket"), - method: .get - ) + let (data, _) = try await execute( + for: HTTPRequest( + method: .get, + url: configuration.url.appendingPathComponent("bucket") + ), + from: nil ) - .decoded(decoder: configuration.decoder) + + return try configuration.decoder.decode([Bucket].self, from: data) } /// Retrieves the details of an existing Storage bucket. /// - Parameters: /// - id: The unique identifier of the bucket you would like to retrieve. public func getBucket(_ id: String) async throws -> Bucket { - try await execute( - HTTPRequest( - url: configuration.url.appendingPathComponent("bucket/\(id)"), - method: .get - ) + let (data, _) = try await execute( + for: HTTPRequest( + method: .get, + url: configuration.url.appendingPathComponent("bucket/\(id)") + ), + from: nil ) - .decoded(decoder: configuration.decoder) + + return try configuration.decoder.decode(Bucket.self, from: data) } struct BucketParameters: Encodable { @@ -45,17 +50,17 @@ public class StorageBucketApi: StorageApi, @unchecked Sendable { /// - options: Options for creating the bucket. public func createBucket(_ id: String, options: BucketOptions = .init()) async throws { try await execute( - HTTPRequest( - url: configuration.url.appendingPathComponent("bucket"), + for: HTTPRequest( method: .post, - body: configuration.encoder.encode( - BucketParameters( - id: id, - name: id, - public: options.public, - fileSizeLimit: options.fileSizeLimit, - allowedMimeTypes: options.allowedMimeTypes - ) + url: configuration.url.appendingPathComponent("bucket") + ), + from: configuration.encoder.encode( + BucketParameters( + id: id, + name: id, + public: options.public, + fileSizeLimit: options.fileSizeLimit, + allowedMimeTypes: options.allowedMimeTypes ) ) ) @@ -67,17 +72,17 @@ public class StorageBucketApi: StorageApi, @unchecked Sendable { /// - options: Options for updating the bucket. public func updateBucket(_ id: String, options: BucketOptions) async throws { try await execute( - HTTPRequest( - url: configuration.url.appendingPathComponent("bucket/\(id)"), + for: HTTPRequest( method: .put, - body: configuration.encoder.encode( - BucketParameters( - id: id, - name: id, - public: options.public, - fileSizeLimit: options.fileSizeLimit, - allowedMimeTypes: options.allowedMimeTypes - ) + url: configuration.url.appendingPathComponent("bucket/\(id)") + ), + from: configuration.encoder.encode( + BucketParameters( + id: id, + name: id, + public: options.public, + fileSizeLimit: options.fileSizeLimit, + allowedMimeTypes: options.allowedMimeTypes ) ) ) @@ -88,10 +93,11 @@ public class StorageBucketApi: StorageApi, @unchecked Sendable { /// - id: The unique identifier of the bucket you would like to empty. public func emptyBucket(_ id: String) async throws { try await execute( - HTTPRequest( - url: configuration.url.appendingPathComponent("bucket/\(id)/empty"), - method: .post - ) + for: HTTPRequest( + method: .post, + url: configuration.url.appendingPathComponent("bucket/\(id)/empty") + ), + from: nil ) } @@ -101,10 +107,11 @@ public class StorageBucketApi: StorageApi, @unchecked Sendable { /// - id: The unique identifier of the bucket you would like to delete. public func deleteBucket(_ id: String) async throws { try await execute( - HTTPRequest( - url: configuration.url.appendingPathComponent("bucket/\(id)"), - method: .delete - ) + for: HTTPRequest( + method: .delete, + url: configuration.url.appendingPathComponent("bucket/\(id)") + ), + from: nil ) } } diff --git a/Sources/Storage/StorageFileApi.swift b/Sources/Storage/StorageFileApi.swift index cfcbef21..82ac8126 100644 --- a/Sources/Storage/StorageFileApi.swift +++ b/Sources/Storage/StorageFileApi.swift @@ -103,17 +103,18 @@ public class StorageFileApi: StorageApi, @unchecked Sendable { let cleanPath = _removeEmptyFolders(path) let _path = _getFinalPath(cleanPath) - let response = try await execute( - HTTPRequest( - url: configuration.url.appendingPathComponent("object/\(_path)"), + let (data, _) = try await execute( + for: HTTPRequest( method: method, - query: [], - formData: formData, + url: configuration.url.appendingPathComponent("object/\(_path)"), options: options, - headers: headers - ) + headers: headers, + formData: formData + ), + from: formData.encode() ) - .decoded(as: UploadResponse.self, decoder: configuration.decoder) + + let response = try configuration.decoder.decode(UploadResponse.self, from: data) return FileUploadResponse( id: response.Id, @@ -209,17 +210,17 @@ public class StorageFileApi: StorageApi, @unchecked Sendable { options: DestinationOptions? = nil ) async throws { try await execute( - HTTPRequest( - url: configuration.url.appendingPathComponent("object/move"), + for: HTTPRequest( method: .post, - body: configuration.encoder.encode( - [ - "bucketId": bucketId, - "sourceKey": source, - "destinationKey": destination, - "destinationBucket": options?.destinationBucket, - ] - ) + url: configuration.url.appendingPathComponent("object/move") + ), + from: configuration.encoder.encode( + [ + "bucketId": bucketId, + "sourceKey": source, + "destinationKey": destination, + "destinationBucket": options?.destinationBucket, + ] ) ) } @@ -239,22 +240,23 @@ public class StorageFileApi: StorageApi, @unchecked Sendable { let Key: String } - return try await execute( - HTTPRequest( - url: configuration.url.appendingPathComponent("object/copy"), + let (data, _) = try await execute( + for: HTTPRequest( method: .post, - body: configuration.encoder.encode( - [ - "bucketId": bucketId, - "sourceKey": source, - "destinationKey": destination, - "destinationBucket": options?.destinationBucket, - ] - ) + url: configuration.url.appendingPathComponent("object/copy") + ), + from: configuration.encoder.encode( + [ + "bucketId": bucketId, + "sourceKey": source, + "destinationKey": destination, + "destinationBucket": options?.destinationBucket, + ] ) ) - .decoded(as: UploadResponse.self, decoder: configuration.decoder) - .Key + + let response = try configuration.decoder.decode(UploadResponse.self, from: data) + return response.Key } /// Creates a signed URL. Use a signed URL to share a file for a fixed amount of time. @@ -276,16 +278,17 @@ public class StorageFileApi: StorageApi, @unchecked Sendable { let encoder = JSONEncoder() - let response = try await execute( - HTTPRequest( - url: configuration.url.appendingPathComponent("object/sign/\(bucketId)/\(path)"), + let (data, _) = try await execute( + for: HTTPRequest( method: .post, - body: encoder.encode( - Body(expiresIn: expiresIn, transform: transform) - ) + url: configuration.url.appendingPathComponent("object/sign/\(bucketId)/\(path)") + ), + from: encoder.encode( + Body(expiresIn: expiresIn, transform: transform) ) ) - .decoded(as: SignedURLResponse.self, decoder: configuration.decoder) + + let response = try configuration.decoder.decode(SignedURLResponse.self, from: data) return try makeSignedURL(response.signedURL, download: download) } @@ -327,16 +330,17 @@ public class StorageFileApi: StorageApi, @unchecked Sendable { let encoder = JSONEncoder() - let response = try await execute( - HTTPRequest( - url: configuration.url.appendingPathComponent("object/sign/\(bucketId)"), + let (data, _) = try await execute( + for: HTTPRequest( method: .post, - body: encoder.encode( - Params(expiresIn: expiresIn, paths: paths) - ) + url: configuration.url.appendingPathComponent("object/sign/\(bucketId)") + ), + from: encoder.encode( + Params(expiresIn: expiresIn, paths: paths) ) ) - .decoded(as: [SignedURLResponse].self, decoder: configuration.decoder) + + let response = try configuration.decoder.decode([SignedURLResponse].self, from: data) return try response.map { try makeSignedURL($0.signedURL, download: download) } } @@ -384,14 +388,15 @@ public class StorageFileApi: StorageApi, @unchecked Sendable { /// - paths: An array of files to be deletes, including the path and file name. For example [`folder/image.png`]. /// - Returns: A list of removed ``FileObject``. public func remove(paths: [String]) async throws -> [FileObject] { - try await execute( - HTTPRequest( - url: configuration.url.appendingPathComponent("object/\(bucketId)"), + let (data, _) = try await execute( + for: HTTPRequest( method: .delete, - body: configuration.encoder.encode(["prefixes": paths]) - ) + url: configuration.url.appendingPathComponent("object/\(bucketId)") + ), + from: configuration.encoder.encode(["prefixes": paths]) ) - .decoded(decoder: configuration.decoder) + + return try configuration.decoder.decode([FileObject].self, from: data) } /// Lists all the files within a bucket. @@ -407,14 +412,15 @@ public class StorageFileApi: StorageApi, @unchecked Sendable { var options = options ?? defaultSearchOptions options.prefix = path ?? "" - return try await execute( - HTTPRequest( - url: configuration.url.appendingPathComponent("object/list/\(bucketId)"), + let (data, _) = try await execute( + for: HTTPRequest( method: .post, - body: encoder.encode(options) - ) + url: configuration.url.appendingPathComponent("object/list/\(bucketId)") + ), + from: encoder.encode(options) ) - .decoded(decoder: configuration.decoder) + + return try configuration.decoder.decode([FileObject].self, from: data) } /// Downloads a file from a private bucket. For public buckets, make a request to the URL returned @@ -431,38 +437,43 @@ public class StorageFileApi: StorageApi, @unchecked Sendable { let renderPath = options != nil ? "render/image/authenticated" : "object" let _path = _getFinalPath(path) - return try await execute( - HTTPRequest( - url: configuration.url - .appendingPathComponent("\(renderPath)/\(_path)"), + let (data, _) = try await execute( + for: HTTPRequest( method: .get, - query: queryItems - ) + url: configuration.url + .appendingPathComponent("\(renderPath)/\(_path)") + .appendingQueryItems(queryItems) + ), + from: nil ) - .data + + return data } /// Retrieves the details of an existing file. public func info(path: String) async throws -> FileObjectV2 { let _path = _getFinalPath(path) - return try await execute( - HTTPRequest( - url: configuration.url.appendingPathComponent("object/info/\(_path)"), - method: .get - ) + let (data, _) = try await execute( + for: HTTPRequest( + method: .get, + url: configuration.url.appendingPathComponent("object/info/\(_path)") + ), + from: nil ) - .decoded(decoder: configuration.decoder) + + return try configuration.decoder.decode(FileObjectV2.self, from: data) } /// Checks the existence of file. public func exists(path: String) async throws -> Bool { do { try await execute( - HTTPRequest( - url: configuration.url.appendingPathComponent("object/\(bucketId)/\(path)"), - method: .head - ) + for: HTTPRequest( + method: .head, + url: configuration.url.appendingPathComponent("object/\(bucketId)/\(path)") + ), + from: nil ) return true } catch { @@ -471,7 +482,7 @@ public class StorageFileApi: StorageApi, @unchecked Sendable { if let error = error as? StorageError { statusCode = error.statusCode.flatMap(Int.init) } else if let error = error as? HTTPError { - statusCode = error.response.statusCode + statusCode = error.response.status.code } if let statusCode, [400, 404].contains(statusCode) { @@ -553,14 +564,16 @@ public class StorageFileApi: StorageApi, @unchecked Sendable { headers[.xUpsert] = "true" } - let response = try await execute( - HTTPRequest( - url: configuration.url.appendingPathComponent("object/upload/sign/\(bucketId)/\(path)"), + let (data, _) = try await execute( + for: HTTPRequest( method: .post, - headers: headers - ) + url: configuration.url.appendingPathComponent("object/upload/sign/\(bucketId)/\(path)"), + headerFields: headers + ), + from: nil ) - .decoded(as: Response.self, decoder: configuration.decoder) + + let response = try configuration.decoder.decode(Response.self, from: data) let signedURL = try makeSignedURL(response.url, download: nil) @@ -646,19 +659,20 @@ public class StorageFileApi: StorageApi, @unchecked Sendable { let Key: String } - let fullPath = try await execute( - HTTPRequest( - url: configuration.url - .appendingPathComponent("object/upload/sign/\(bucketId)/\(path)"), + let (data, _) = try await execute( + for: HTTPRequest( method: .put, - query: [URLQueryItem(name: "token", value: token)], - formData: formData, + url: configuration.url + .appendingPathComponent("object/upload/sign/\(bucketId)/\(path)") + .appendingQueryItems([URLQueryItem(name: "token", value: token)]), options: options, - headers: headers - ) + headers: headers, + formData: formData + ), + from: formData.encode() ) - .decoded(as: UploadResponse.self, decoder: configuration.decoder) - .Key + + let fullPath = try configuration.decoder.decode(UploadResponse.self, from: data).Key return SignedURLUploadResponse(path: path, fullPath: fullPath) } diff --git a/Sources/Storage/StorageHTTPClient.swift b/Sources/Storage/StorageHTTPClient.swift index b078f701..17a3221c 100644 --- a/Sources/Storage/StorageHTTPClient.swift +++ b/Sources/Storage/StorageHTTPClient.swift @@ -1,28 +1,35 @@ import Foundation +import HTTPTypes +import HTTPTypesFoundation #if canImport(FoundationNetworking) import FoundationNetworking #endif public struct StorageHTTPSession: Sendable { - public var fetch: @Sendable (_ request: URLRequest) async throws -> (Data, URLResponse) - public var upload: - @Sendable (_ request: URLRequest, _ data: Data) async throws -> (Data, URLResponse) + public var fetch: @Sendable ( + _ request: HTTPRequest, + _ bodyData: Data? + ) async throws -> (Data, HTTPResponse) public init( - fetch: @escaping @Sendable (_ request: URLRequest) async throws -> (Data, URLResponse), - upload: @escaping @Sendable (_ request: URLRequest, _ data: Data) async throws -> ( - Data, URLResponse - ) + fetch: @escaping @Sendable ( + _ request: HTTPRequest, + _ bodyData: Data? + ) async throws -> (Data, HTTPResponse) ) { self.fetch = fetch - self.upload = upload } public init(session: URLSession = .shared) { self.init( - fetch: { try await session.data(for: $0) }, - upload: { try await session.upload(for: $0, from: $1) } + fetch: { request, bodyData in + if let bodyData { + try await session.upload(for: request, from: bodyData) + } else { + try await session.data(for: request) + } + } ) } } diff --git a/Sources/Storage/SupabaseStorage.swift b/Sources/Storage/SupabaseStorage.swift index ed57a66e..21415495 100644 --- a/Sources/Storage/SupabaseStorage.swift +++ b/Sources/Storage/SupabaseStorage.swift @@ -1,4 +1,5 @@ import Foundation +import HTTPTypes import Helpers public typealias SupabaseLogger = Helpers.SupabaseLogger @@ -6,7 +7,7 @@ public typealias SupabaseLogMessage = Helpers.SupabaseLogMessage public struct StorageClientConfiguration: Sendable { public let url: URL - public var headers: [String: String] + public var headers: HTTPFields public let encoder: JSONEncoder public let decoder: JSONDecoder public let session: StorageHTTPSession @@ -14,7 +15,7 @@ public struct StorageClientConfiguration: Sendable { public init( url: URL, - headers: [String: String], + headers: HTTPFields, encoder: JSONEncoder = .defaultStorageEncoder, decoder: JSONDecoder = .defaultStorageDecoder, session: StorageHTTPSession = .init(), diff --git a/Sources/Storage/Types.swift b/Sources/Storage/Types.swift index d47be699..b1fa4826 100644 --- a/Sources/Storage/Types.swift +++ b/Sources/Storage/Types.swift @@ -1,4 +1,5 @@ import Foundation +import HTTPTypes import Helpers public struct SearchOptions: Encodable, Sendable { @@ -63,7 +64,7 @@ public struct FileOptions: Sendable { public var metadata: [String: AnyJSON]? /// Optionally add extra headers. - public var headers: [String: String]? + public var headers: HTTPFields? public init( cacheControl: String = "3600", @@ -71,7 +72,7 @@ public struct FileOptions: Sendable { upsert: Bool = false, duplex: String? = nil, metadata: [String: AnyJSON]? = nil, - headers: [String: String]? = nil + headers: HTTPFields? = nil ) { self.cacheControl = cacheControl self.contentType = contentType diff --git a/Sources/Supabase/SupabaseClient.swift b/Sources/Supabase/SupabaseClient.swift index 28c75dbb..ff88ef9f 100644 --- a/Sources/Supabase/SupabaseClient.swift +++ b/Sources/Supabase/SupabaseClient.swift @@ -2,12 +2,12 @@ import ConcurrencyExtras import Foundation @_exported import Functions +import HTTPTypes import Helpers import IssueReporting @_exported import PostgREST @_exported import Realtime @_exported import Storage -import HTTPTypes #if canImport(FoundationNetworking) import FoundationNetworking @@ -47,9 +47,15 @@ public final class SupabaseClient: Sendable { $0.rest = PostgrestClient( url: databaseURL, schema: options.db.schema, - headers: headers, + headers: _headers, logger: options.global.logger, - fetch: fetchWithAuth, + fetch: { request, bodyData in + if let bodyData { + return try await self.uploadWithAuth(for: request, from: bodyData) + } else { + return try await self.fetchWithAuth(for: request) + } + }, encoder: options.db.encoder, decoder: options.db.decoder ) @@ -66,8 +72,14 @@ public final class SupabaseClient: Sendable { $0.storage = SupabaseStorageClient( configuration: StorageClientConfiguration( url: storageURL, - headers: headers, - session: StorageHTTPSession(fetch: fetchWithAuth, upload: uploadWithAuth), + headers: _headers, + session: StorageHTTPSession { request, bodyData in + if let bodyData { + return try await self.uploadWithAuth(for: request, from: bodyData) + } else { + return try await self.fetchWithAuth(for: request) + } + }, logger: options.global.logger ) ) @@ -88,10 +100,16 @@ public final class SupabaseClient: Sendable { if $0.functions == nil { $0.functions = FunctionsClient( url: functionsURL, - headers: headers, + headers: _headers, region: options.functions.region, logger: options.global.logger, - fetch: fetchWithAuth + fetch: { request, bodyData in + if let bodyData { + return try await self.uploadWithAuth(for: request, from: bodyData) + } else { + return try await self.fetchWithAuth(for: request) + } + } ) } @@ -154,19 +172,19 @@ public final class SupabaseClient: Sendable { databaseURL = supabaseURL.appendingPathComponent("/rest/v1") functionsURL = supabaseURL.appendingPathComponent("/functions/v1") - _headers = HTTPFields([ - "X-Client-Info": "supabase-swift/\(version)", - "Authorization": "Bearer \(supabaseKey)", - "Apikey": supabaseKey, - ]) - .merging(with: HTTPFields(options.global.headers)) + let headers: HTTPFields = [ + .xClientInfo: "supabase-swift/\(version)", + .authorization: "Bearer \(supabaseKey)", + .apiKey: supabaseKey, + ] + _headers = headers.merging(with: options.global.headers) // default storage key uses the supabase project ref as a namespace let defaultStorageKey = "sb-\(supabaseURL.host!.split(separator: ".")[0])-auth-token" _auth = AuthClient( url: supabaseURL.appendingPathComponent("/auth/v1"), - headers: _headers.dictionary, + headers: _headers, flowType: options.auth.flowType, redirectToURL: options.auth.redirectToURL, storageKey: options.auth.storageKey ?? defaultStorageKey, @@ -174,9 +192,13 @@ public final class SupabaseClient: Sendable { logger: options.global.logger, encoder: options.auth.encoder, decoder: options.auth.decoder, - fetch: { + fetch: { request, bodyData in // DON'T use `fetchWithAuth` method within the AuthClient as it may cause a deadlock. - try await options.global.session.data(for: $0) + if let bodyData { + try await options.global.session.upload(for: request, from: bodyData) + } else { + try await options.global.session.data(for: request) + } }, autoRefreshToken: options.auth.autoRefreshToken ) @@ -184,14 +206,13 @@ public final class SupabaseClient: Sendable { _realtime = UncheckedSendable( RealtimeClient( supabaseURL.appendingPathComponent("/realtime/v1").absoluteString, - headers: _headers.dictionary, + headers: _headers, params: _headers.dictionary ) ) var realtimeOptions = options.realtime realtimeOptions.headers.merge(with: _headers) - if realtimeOptions.logger == nil { realtimeOptions.logger = options.global.logger } @@ -338,28 +359,29 @@ public final class SupabaseClient: Sendable { } @Sendable - private func fetchWithAuth(_ request: URLRequest) async throws -> (Data, URLResponse) { - try await session.data(for: adapt(request: request)) + private func fetchWithAuth(for request: HTTPRequest) async throws -> (Data, HTTPResponse) { + return try await session.data(for: adapt(request: request)) } @Sendable private func uploadWithAuth( - _ request: URLRequest, + for request: HTTPRequest, from data: Data - ) async throws -> (Data, URLResponse) { + ) async throws -> (Data, HTTPResponse) { try await session.upload(for: adapt(request: request), from: data) } - private func adapt(request: URLRequest) async -> URLRequest { - let token: String? = if let accessToken = options.auth.accessToken { - try? await accessToken() - } else { - try? await auth.session.accessToken - } + private func adapt(request: HTTPRequest) async -> HTTPRequest { + let token: String? = + if let accessToken = options.auth.accessToken { + try? await accessToken() + } else { + try? await auth.session.accessToken + } var request = request if let token { - request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + request.headerFields[.authorization] = "Bearer \(token)" } return request } diff --git a/Sources/Supabase/Types.swift b/Sources/Supabase/Types.swift index 754515ca..29797ccc 100644 --- a/Sources/Supabase/Types.swift +++ b/Sources/Supabase/Types.swift @@ -1,5 +1,6 @@ import Auth import Foundation +import HTTPTypes import Helpers import PostgREST import Realtime @@ -89,7 +90,7 @@ public struct SupabaseClientOptions: Sendable { public struct GlobalOptions: Sendable { /// Optional headers for initializing the client, it will be passed down to all sub-clients. - public let headers: [String: String] + public let headers: HTTPFields /// A session to use for making requests, defaults to `URLSession.shared`. public let session: URLSession @@ -98,7 +99,7 @@ public struct SupabaseClientOptions: Sendable { public let logger: (any SupabaseLogger)? public init( - headers: [String: String] = [:], + headers: HTTPFields = [:], session: URLSession = .shared, logger: (any SupabaseLogger)? = nil ) { From 9322ce25e393cf8825e9c571974bfd38d1951f52 Mon Sep 17 00:00:00 2001 From: "zunda.dev@gmail.com" Date: Sat, 30 Nov 2024 13:10:08 +0900 Subject: [PATCH 2/3] fix Test --- Sources/Auth/AuthClient.swift | 8 +- Sources/Auth/AuthClientConfiguration.swift | 2 +- Sources/Auth/Internal/APIClient.swift | 7 +- Sources/Auth/Internal/SessionStorage.swift | 3 +- Sources/Functions/FunctionsClient.swift | 6 +- Sources/Functions/Types.swift | 2 +- Sources/Helpers/HTTP/HTTPClient.swift | 7 +- Sources/Helpers/HTTP/HTTPFields.swift | 16 ++- Sources/Helpers/HTTP/LoggerInterceptor.swift | 4 + .../Helpers/HTTP/URLSession+HTTPRequest.swift | 116 ++++++++++++++++ Sources/PostgREST/PostgrestBuilder.swift | 6 +- Sources/PostgREST/PostgrestClient.swift | 2 +- Sources/Realtime/PhoenixTransport.swift | 18 +-- Sources/Realtime/RealtimeClient.swift | 2 +- Sources/Realtime/V2/RealtimeChannelV2.swift | 9 +- Sources/Realtime/V2/Types.swift | 21 +-- Sources/Storage/StorageApi.swift | 2 +- Sources/Storage/StorageFileApi.swift | 4 +- Sources/Storage/StorageHTTPClient.swift | 9 +- Sources/Supabase/SupabaseClient.swift | 21 ++- Sources/TestHelpers/HTTPClientMock.swift | 31 +++-- .../AuthClientMultipleInstancesTests.swift | 3 +- Tests/AuthTests/AuthClientTests.swift | 126 ++++++++---------- Tests/AuthTests/AuthErrorTests.swift | 14 +- Tests/AuthTests/ExtractParamsTests.swift | 3 +- Tests/AuthTests/RequestsTests.swift | 44 +++--- Tests/AuthTests/SessionManagerTests.swift | 8 +- Tests/AuthTests/StoredSessionTests.swift | 11 +- .../RequestsTests/testDeleteUser.1.txt | 2 +- .../testGetLinkIdentityURL.1.txt | 2 +- .../RequestsTests/testMFAChallenge.1.txt | 2 +- .../RequestsTests/testMFAChallengePhone.1.txt | 2 +- .../RequestsTests/testMFAEnrollLegacy.1.txt | 2 +- .../RequestsTests/testMFAEnrollPhone.1.txt | 2 +- .../RequestsTests/testMFAEnrollTotp.1.txt | 2 +- .../RequestsTests/testMFAUnenroll.1.txt | 2 +- .../RequestsTests/testMFAVerify.1.txt | 2 +- .../RequestsTests/testReauthenticate.1.txt | 2 +- .../RequestsTests/testRefreshSession.1.txt | 2 +- .../RequestsTests/testResendEmail.1.txt | 2 +- .../RequestsTests/testResendPhone.1.txt | 2 +- .../testResetPasswordForEmail.1.txt | 2 +- .../RequestsTests/testSessionFromURL.1.txt | 2 +- .../testSetSessionWithAExpiredToken.1.txt | 2 +- ...tSetSessionWithAFutureExpirationDate.1.txt | 2 +- .../RequestsTests/testSignInAnonymously.1.txt | 2 +- .../testSignInWithEmailAndPassword.1.txt | 2 +- .../RequestsTests/testSignInWithIdToken.1.txt | 2 +- .../testSignInWithOTPUsingEmail.1.txt | 2 +- .../testSignInWithOTPUsingPhone.1.txt | 2 +- .../testSignInWithPhoneAndPassword.1.txt | 2 +- .../testSignInWithSSOUsingDomain.1.txt | 2 +- .../testSignInWithSSOUsingProviderId.1.txt | 2 +- .../RequestsTests/testSignOut.1.txt | 2 +- .../testSignOutWithLocalScope.1.txt | 2 +- .../testSignOutWithOthersScope.1.txt | 2 +- .../testSignUpWithEmailAndPassword.1.txt | 2 +- .../testSignUpWithPhoneAndPassword.1.txt | 2 +- .../RequestsTests/testUnlinkIdentity.1.txt | 2 +- .../RequestsTests/testUpdateUser.1.txt | 2 +- .../testVerifyOTPUsingEmail.1.txt | 2 +- .../testVerifyOTPUsingPhone.1.txt | 2 +- .../testVerifyOTPUsingTokenHash.1.txt | 2 +- .../FunctionInvokeOptionsTests.swift | 4 +- .../FunctionsTests/FunctionsClientTests.swift | 90 ++++++------- Tests/FunctionsTests/RequestTests.swift | 27 +++- .../RequestTests/testInvokeWithBody.1.txt | 4 +- .../testInvokeWithCustomHeader.1.txt | 4 +- .../testInvokeWithCustomMethod.1.txt | 4 +- .../testInvokeWithCustomRegion.1.txt | 4 +- .../testInvokeWithDefaultOptions.1.txt | 4 +- .../HelpersTests/ObservationTokenTests.swift | 3 +- .../AuthClientIntegrationTests.swift | 7 +- .../PostgrestIntegrationTests.swift | 2 +- .../Potsgrest/PostgresTransformsTests.swift | 2 +- .../Potsgrest/PostgrestBasicTests.swift | 2 +- .../Potsgrest/PostgrestFilterTests.swift | 2 +- .../PostgrestResourceEmbeddingTests.swift | 2 +- .../RealtimeIntegrationTests.swift | 15 ++- .../StorageClientIntegrationTests.swift | 7 +- .../StorageFileIntegrationTests.swift | 15 ++- .../PostgRESTTests/BuildURLRequestTests.swift | 33 ++--- Tests/PostgRESTTests/JSONTests.swift | 11 +- .../PostgrestBuilderTests.swift | 14 +- .../PostgrestResponseTests.swift | 21 ++- Tests/RealtimeTests/MockWebSocketClient.swift | 3 +- .../PostgresJoinConfigTests.swift | 3 +- Tests/RealtimeTests/RealtimeTests.swift | 25 ++-- Tests/RealtimeTests/_PushTests.swift | 5 +- .../SupabaseStorageClient+Test.swift | 6 +- Tests/StorageTests/SupabaseStorageTests.swift | 89 ++++++------- Tests/SupabaseTests/SupabaseClientTests.swift | 30 +++-- 92 files changed, 586 insertions(+), 427 deletions(-) create mode 100644 Sources/Helpers/HTTP/URLSession+HTTPRequest.swift diff --git a/Sources/Auth/AuthClient.swift b/Sources/Auth/AuthClient.swift index 12dc3e43..ea192f2c 100644 --- a/Sources/Auth/AuthClient.swift +++ b/Sources/Auth/AuthClient.swift @@ -1122,10 +1122,12 @@ public final class AuthClient: Sendable { if let jwt { request.headerFields[.authorization] = "Bearer \(jwt)" + let (data, _) = try await api.execute(for: request, from: nil) + return try configuration.decoder.decode(User.self, from: data) + } else { + let (data, _) = try await api.authorizedExecute(for: request, from: nil) + return try configuration.decoder.decode(User.self, from: data) } - - let (data, _) = try await api.authorizedExecute(for: request, from: nil) - return try configuration.decoder.decode(User.self, from: data) } /// Updates user data, if there is a logged in user. diff --git a/Sources/Auth/AuthClientConfiguration.swift b/Sources/Auth/AuthClientConfiguration.swift index 17289a85..c7917599 100644 --- a/Sources/Auth/AuthClientConfiguration.swift +++ b/Sources/Auth/AuthClientConfiguration.swift @@ -83,7 +83,7 @@ extension AuthClient { }, autoRefreshToken: Bool = AuthClient.Configuration.defaultAutoRefreshToken ) { - let headers = headers.merging(with: Configuration.defaultHeaders) + let headers = Configuration.defaultHeaders.merging(headers) { $1 } self.url = url ?? defaultAuthURL self.headers = headers diff --git a/Sources/Auth/Internal/APIClient.swift b/Sources/Auth/Internal/APIClient.swift index 2a9e36c8..2abd1c00 100644 --- a/Sources/Auth/Internal/APIClient.swift +++ b/Sources/Auth/Internal/APIClient.swift @@ -35,12 +35,9 @@ struct APIClient: Sendable { func execute( for request: HTTPRequest, from bodyData: Data? - ) async throws -> ( - Data, - HTTPResponse - ) { + ) async throws -> (Data, HTTPResponse) { var request = request - request.headerFields = HTTPFields(configuration.headers).merging(with: request.headerFields) + request.headerFields = request.headerFields.merging(configuration.headers) { $1 } if request.headerFields[.apiVersionHeaderName] == nil { request.headerFields[.apiVersionHeaderName] = apiVersions[._20240101]!.name.rawValue diff --git a/Sources/Auth/Internal/SessionStorage.swift b/Sources/Auth/Internal/SessionStorage.swift index 29be29b7..f23dd083 100644 --- a/Sources/Auth/Internal/SessionStorage.swift +++ b/Sources/Auth/Internal/SessionStorage.swift @@ -125,7 +125,8 @@ extension StorageMigration { let storedSession = try? AuthClient.Configuration.jsonDecoder.decode( StoredSession.self, from: data - ) { + ) + { let session = try AuthClient.Configuration.jsonEncoder.encode(storedSession.session) try storage.store(key: key, value: session) } diff --git a/Sources/Functions/FunctionsClient.swift b/Sources/Functions/FunctionsClient.swift index 45812090..b35f6272 100644 --- a/Sources/Functions/FunctionsClient.swift +++ b/Sources/Functions/FunctionsClient.swift @@ -236,9 +236,13 @@ public final class FunctionsClient: Sendable { url: url .appendingPathComponent(functionName) .appendingQueryItems(options.query), - headerFields: mutableState.headers.merging(with: options.headers) + headerFields: mutableState.headers.merging(options.headers) { $1 } ) + if options.body != nil && request.headerFields[.contentType] == nil { + request.headerFields[.contentType] = "application/json" + } + if let region = options.region ?? region { request.headerFields[.xRegion] = region } diff --git a/Sources/Functions/Types.swift b/Sources/Functions/Types.swift index afa734fc..f7356f07 100644 --- a/Sources/Functions/Types.swift +++ b/Sources/Functions/Types.swift @@ -65,7 +65,7 @@ public struct FunctionInvokeOptions: Sendable { } self.method = method - self.headers = defaultHeaders.merging(with: headers) + self.headers = defaultHeaders.merging(headers) { $1 } self.region = region self.query = query } diff --git a/Sources/Helpers/HTTP/HTTPClient.swift b/Sources/Helpers/HTTP/HTTPClient.swift index 09e83170..03b69b41 100644 --- a/Sources/Helpers/HTTP/HTTPClient.swift +++ b/Sources/Helpers/HTTP/HTTPClient.swift @@ -33,7 +33,12 @@ package actor HTTPClient: HTTPClientType { _ bodyData: Data? ) async throws -> (Data, HTTPResponse) { var next: @Sendable (HTTPRequest, Data?) async throws -> (Data, HTTPResponse) = { - return try await self.fetch($0, $1) + request, bodyData in + var request = request + if bodyData != nil && request.headerFields[.contentType] == nil { + request.headerFields[.contentType] = "application/json" + } + return try await self.fetch(request, bodyData) } for interceptor in interceptors.reversed() { diff --git a/Sources/Helpers/HTTP/HTTPFields.swift b/Sources/Helpers/HTTP/HTTPFields.swift index 141e1927..b900632f 100644 --- a/Sources/Helpers/HTTP/HTTPFields.swift +++ b/Sources/Helpers/HTTP/HTTPFields.swift @@ -9,17 +9,21 @@ extension HTTPFields { return .init(keyValues, uniquingKeysWith: { $1 }) } - package mutating func merge(with other: Self) { - for field in other { - self[field.name] = field.value - } + package mutating func merge( + _ other: Self, + uniquingKeysWith combine: (String, String) throws -> String + ) rethrows { + self = try self.merging(other, uniquingKeysWith: combine) } - package func merging(with other: Self) -> Self { + package func merging( + _ other: Self, + uniquingKeysWith combine: (String, String) throws -> String + ) rethrows -> HTTPFields { var copy = self for field in other { - copy[field.name] = field.value + copy[field.name] = try combine(self[field.name] ?? "", field.value) } return copy diff --git a/Sources/Helpers/HTTP/LoggerInterceptor.swift b/Sources/Helpers/HTTP/LoggerInterceptor.swift index 226493be..564115a3 100644 --- a/Sources/Helpers/HTTP/LoggerInterceptor.swift +++ b/Sources/Helpers/HTTP/LoggerInterceptor.swift @@ -31,6 +31,10 @@ package struct LoggerInterceptor: HTTPClientInterceptor { ) do { + var request = request + if bodyData != nil && request.headerFields[.contentType] == nil { + request.headerFields[.contentType] = "application/json" + } let (data, response) = try await next(request, bodyData) logger.verbose( """ diff --git a/Sources/Helpers/HTTP/URLSession+HTTPRequest.swift b/Sources/Helpers/HTTP/URLSession+HTTPRequest.swift new file mode 100644 index 00000000..bdf51860 --- /dev/null +++ b/Sources/Helpers/HTTP/URLSession+HTTPRequest.swift @@ -0,0 +1,116 @@ +import Foundation +import HTTPTypes + +#if canImport(FoundationNetworking) + import FoundationNetworking +#endif + +#if !os(WASI) + + extension URLSessionTask { + /// The original HTTP request this task was created with. + public var originalHTTPRequest: HTTPRequest? { + self.originalRequest?.httpRequest + } + + /// The current HTTP request -- may differ from the `originalHTTPRequest` due to HTTP redirection. + public var currentHTTPRequest: HTTPRequest? { + self.currentRequest?.httpRequest + } + + /// The HTTP response received from the server. + public var httpResponse: HTTPResponse? { + (self.response as? HTTPURLResponse)?.httpResponse + } + } + + private enum HTTPTypeConversionError: Error { + case failedToConvertHTTPRequestToURLRequest + case failedToConvertURLResponseToHTTPResponse + } + +#endif + +#if canImport(FoundationNetworking) && compiler(<6) + + @available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) + extension URLSession { + /// Convenience method to load data using an `HTTPRequest`; creates and resumes a `URLSessionDataTask` internally. + /// + /// - Parameter request: The `HTTPRequest` for which to load data. + /// - Parameter delegate: Task-specific delegate. + /// - Returns: Data and response. + public func data( + for request: HTTPRequest, + delegate: (any URLSessionTaskDelegate)? = nil + ) async throws -> (Data, HTTPResponse) { + guard let urlRequest = URLRequest(httpRequest: request) else { + throw HTTPTypeConversionError.failedToConvertHTTPRequestToURLRequest + } + let (data, urlResponse) = try await self.data(for: urlRequest, delegate: delegate) + guard let response = (urlResponse as? HTTPURLResponse)?.httpResponse else { + throw HTTPTypeConversionError.failedToConvertURLResponseToHTTPResponse + } + return (data, response) + } + + /// Convenience method to upload data using an `HTTPRequest`, creates and resumes a `URLSessionUploadTask` internally. + /// + /// - Parameter request: The `HTTPRequest` for which to upload data. + /// - Parameter bodyData: Data to upload. + /// - Parameter delegate: Task-specific delegate. + /// - Returns: Data and response. + public func upload( + for request: HTTPRequest, + from bodyData: Data, + delegate: (any URLSessionTaskDelegate)? = nil + ) async throws -> (Data, HTTPResponse) { + guard let urlRequest = URLRequest(httpRequest: request) else { + throw HTTPTypeConversionError.failedToConvertHTTPRequestToURLRequest + } + let (data, urlResponse) = try await self.upload( + for: urlRequest, from: bodyData, delegate: delegate) + guard let response = (urlResponse as? HTTPURLResponse)?.httpResponse else { + throw HTTPTypeConversionError.failedToConvertURLResponseToHTTPResponse + } + return (data, response) + } + } + + @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) + extension URLSession { + /// Convenience method to load data using an `HTTPRequest`; creates and resumes a `URLSessionDataTask` internally. + /// + /// - Parameter request: The `HTTPRequest` for which to load data. + /// - Returns: Data and response. + public func data(for request: HTTPRequest) async throws -> (Data, HTTPResponse) { + guard let urlRequest = URLRequest(httpRequest: request) else { + throw HTTPTypeConversionError.failedToConvertHTTPRequestToURLRequest + } + let (data, urlResponse) = try await self.data(for: urlRequest) + guard let response = (urlResponse as? HTTPURLResponse)?.httpResponse else { + throw HTTPTypeConversionError.failedToConvertURLResponseToHTTPResponse + } + return (data, response) + } + + /// Convenience method to upload data using an `HTTPRequest`, creates and resumes a `URLSessionUploadTask` internally. + /// + /// - Parameter request: The `HTTPRequest` for which to upload data. + /// - Parameter bodyData: Data to upload. + /// - Returns: Data and response. + public func upload(for request: HTTPRequest, from bodyData: Data) async throws -> ( + Data, HTTPResponse + ) { + guard let urlRequest = URLRequest(httpRequest: request) else { + throw HTTPTypeConversionError.failedToConvertHTTPRequestToURLRequest + } + let (data, urlResponse) = try await self.upload(for: urlRequest, from: bodyData) + guard let response = (urlResponse as? HTTPURLResponse)?.httpResponse else { + throw HTTPTypeConversionError.failedToConvertURLResponseToHTTPResponse + } + return (data, response) + } + } + +#endif diff --git a/Sources/PostgREST/PostgrestBuilder.swift b/Sources/PostgREST/PostgrestBuilder.swift index 3726a97a..8eb1c411 100644 --- a/Sources/PostgREST/PostgrestBuilder.swift +++ b/Sources/PostgREST/PostgrestBuilder.swift @@ -102,7 +102,7 @@ public class PostgrestBuilder: @unchecked Sendable { options: FetchOptions, decode: (Data) throws -> T ) async throws -> PostgrestResponse { - let request = mutableState.withValue { + let (request, bodyData) = mutableState.withValue { $0.fetchOptions = options if $0.fetchOptions.head { @@ -130,10 +130,10 @@ public class PostgrestBuilder: @unchecked Sendable { } } - return $0.request + return ($0.request, $0.bodyData) } - let (data, response) = try await http.send(request, nil) + let (data, response) = try await http.send(request, bodyData) guard 200..<300 ~= response.status.code else { if let error = try? configuration.decoder.decode(PostgrestError.self, from: data) { diff --git a/Sources/PostgREST/PostgrestClient.swift b/Sources/PostgREST/PostgrestClient.swift index 701d07b8..2ba81f04 100644 --- a/Sources/PostgREST/PostgrestClient.swift +++ b/Sources/PostgREST/PostgrestClient.swift @@ -72,7 +72,7 @@ public final class PostgrestClient: Sendable { public init(configuration: Configuration) { _configuration = LockIsolated(configuration) _configuration.withValue { - $0.headers.merge(with: Configuration.defaultHeaders) + $0.headers.merge(Configuration.defaultHeaders) { l, _ in l } } } diff --git a/Sources/Realtime/PhoenixTransport.swift b/Sources/Realtime/PhoenixTransport.swift index 3c416f66..bdc5ee25 100644 --- a/Sources/Realtime/PhoenixTransport.swift +++ b/Sources/Realtime/PhoenixTransport.swift @@ -19,6 +19,7 @@ // THE SOFTWARE. import Foundation +import HTTPTypes #if canImport(FoundationNetworking) import FoundationNetworking @@ -46,7 +47,7 @@ public protocol PhoenixTransport { - Parameters: - headers: Headers to include in the URLRequests when opening the Websocket connection. Can be empty [:] */ - func connect(with headers: [String: String]) + func connect(with headers: HTTPFields) /** Disconnect from the server. @@ -192,20 +193,21 @@ open class URLSessionTransport: NSObject, PhoenixTransport, URLSessionWebSocketD public var readyState: PhoenixTransportReadyState = .closed public var delegate: (any PhoenixTransportDelegate)? = nil - public func connect(with headers: [String: String]) { + public func connect(with headers: HTTPFields) { // Set the transport state as connecting readyState = .connecting // Create the session and websocket task session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil) - var request = URLRequest(url: url) + let request = HTTPRequest( + method: .get, + url: url, + headerFields: headers + ) - for (key, value) in headers { - guard let value = value as? String else { continue } - request.addValue(value, forHTTPHeaderField: key) - } + let urlRequest = URLRequest(httpRequest: request)! - task = session?.webSocketTask(with: request) + task = session?.webSocketTask(with: urlRequest) // Start the task task?.resume() diff --git a/Sources/Realtime/RealtimeClient.swift b/Sources/Realtime/RealtimeClient.swift index eb24440c..49236030 100644 --- a/Sources/Realtime/RealtimeClient.swift +++ b/Sources/Realtime/RealtimeClient.swift @@ -344,7 +344,7 @@ public class RealtimeClient: PhoenixTransportDelegate { // self.connection?.enabledSSLCipherSuites = enabledSSLCipherSuites // #endif - connection?.connect(with: headers.dictionary) + connection?.connect(with: headers) } /// Disconnects the socket diff --git a/Sources/Realtime/V2/RealtimeChannelV2.swift b/Sources/Realtime/V2/RealtimeChannelV2.swift index e0b98fd3..e9c74017 100644 --- a/Sources/Realtime/V2/RealtimeChannelV2.swift +++ b/Sources/Realtime/V2/RealtimeChannelV2.swift @@ -37,10 +37,11 @@ struct Socket: Sendable { var addChannel: @Sendable (_ channel: RealtimeChannelV2) -> Void var removeChannel: @Sendable (_ channel: RealtimeChannelV2) async -> Void var push: @Sendable (_ message: RealtimeMessageV2) async -> Void - var httpSend: @Sendable ( - _ request: HTTPRequest, - _ bodyData: Data? - ) async throws -> (Data, HTTPResponse) + var httpSend: + @Sendable ( + _ request: HTTPRequest, + _ bodyData: Data? + ) async throws -> (Data, HTTPResponse) } extension Socket { diff --git a/Sources/Realtime/V2/Types.swift b/Sources/Realtime/V2/Types.swift index e1172bc9..116ce8c2 100644 --- a/Sources/Realtime/V2/Types.swift +++ b/Sources/Realtime/V2/Types.swift @@ -21,10 +21,13 @@ public struct RealtimeClientOptions: Sendable { var timeoutInterval: TimeInterval var disconnectOnSessionLoss: Bool var connectOnSubscribe: Bool - var fetch: (@Sendable ( - _ request: HTTPRequest, - _ bodyData: Data? - ) async throws -> (Data, HTTPResponse))? + var fetch: + ( + @Sendable ( + _ request: HTTPRequest, + _ bodyData: Data? + ) async throws -> (Data, HTTPResponse) + )? package var logger: (any SupabaseLogger)? public static let defaultHeartbeatInterval: TimeInterval = 15 @@ -40,10 +43,12 @@ public struct RealtimeClientOptions: Sendable { timeoutInterval: TimeInterval = Self.defaultTimeoutInterval, disconnectOnSessionLoss: Bool = Self.defaultDisconnectOnSessionLoss, connectOnSubscribe: Bool = Self.defaultConnectOnSubscribe, - fetch: (@Sendable ( - _ request: HTTPRequest, - _ bodyData: Data? - ) async throws -> (Data, HTTPResponse))? = nil, + fetch: ( + @Sendable ( + _ request: HTTPRequest, + _ bodyData: Data? + ) async throws -> (Data, HTTPResponse) + )? = nil, logger: (any SupabaseLogger)? = nil ) { self.headers = headers diff --git a/Sources/Storage/StorageApi.swift b/Sources/Storage/StorageApi.swift index f22915a7..f2e040f8 100644 --- a/Sources/Storage/StorageApi.swift +++ b/Sources/Storage/StorageApi.swift @@ -35,7 +35,7 @@ public class StorageApi: @unchecked Sendable { from bodyData: Data? ) async throws -> (Data, HTTPResponse) { var request = request - request.headerFields = configuration.headers.merging(with: request.headerFields) + request.headerFields = configuration.headers.merging(request.headerFields) { $1 } let (data, response) = try await http.send(request, bodyData) diff --git a/Sources/Storage/StorageFileApi.swift b/Sources/Storage/StorageFileApi.swift index 82ac8126..91314746 100644 --- a/Sources/Storage/StorageFileApi.swift +++ b/Sources/Storage/StorageFileApi.swift @@ -80,7 +80,7 @@ public class StorageFileApi: StorageApi, @unchecked Sendable { options: FileOptions? ) async throws -> FileUploadResponse { let options = options ?? defaultFileOptions - var headers = options.headers.map { HTTPFields($0) } ?? HTTPFields() + var headers = options.headers ?? HTTPFields() if method == .post { headers[.xUpsert] = "\(options.upsert)" @@ -647,7 +647,7 @@ public class StorageFileApi: StorageApi, @unchecked Sendable { options: FileOptions? ) async throws -> SignedURLUploadResponse { let options = options ?? defaultFileOptions - var headers = options.headers.map { HTTPFields($0) } ?? HTTPFields() + var headers = options.headers ?? HTTPFields() headers[.xUpsert] = "\(options.upsert)" headers[.duplex] = options.duplex diff --git a/Sources/Storage/StorageHTTPClient.swift b/Sources/Storage/StorageHTTPClient.swift index 17a3221c..b16fd55b 100644 --- a/Sources/Storage/StorageHTTPClient.swift +++ b/Sources/Storage/StorageHTTPClient.swift @@ -7,10 +7,11 @@ import HTTPTypesFoundation #endif public struct StorageHTTPSession: Sendable { - public var fetch: @Sendable ( - _ request: HTTPRequest, - _ bodyData: Data? - ) async throws -> (Data, HTTPResponse) + public var fetch: + @Sendable ( + _ request: HTTPRequest, + _ bodyData: Data? + ) async throws -> (Data, HTTPResponse) public init( fetch: @escaping @Sendable ( diff --git a/Sources/Supabase/SupabaseClient.swift b/Sources/Supabase/SupabaseClient.swift index ff88ef9f..93abdff5 100644 --- a/Sources/Supabase/SupabaseClient.swift +++ b/Sources/Supabase/SupabaseClient.swift @@ -47,7 +47,7 @@ public final class SupabaseClient: Sendable { $0.rest = PostgrestClient( url: databaseURL, schema: options.db.schema, - headers: _headers, + headers: headers, logger: options.global.logger, fetch: { request, bodyData in if let bodyData { @@ -72,7 +72,7 @@ public final class SupabaseClient: Sendable { $0.storage = SupabaseStorageClient( configuration: StorageClientConfiguration( url: storageURL, - headers: _headers, + headers: headers, session: StorageHTTPSession { request, bodyData in if let bodyData { return try await self.uploadWithAuth(for: request, from: bodyData) @@ -100,7 +100,7 @@ public final class SupabaseClient: Sendable { if $0.functions == nil { $0.functions = FunctionsClient( url: functionsURL, - headers: _headers, + headers: headers, region: options.functions.region, logger: options.global.logger, fetch: { request, bodyData in @@ -117,13 +117,10 @@ public final class SupabaseClient: Sendable { } } - let _headers: HTTPFields /// Headers provided to the inner clients on initialization. /// /// - Note: This collection is non-mutable, if you want to provide different headers, pass it in ``SupabaseClientOptions/GlobalOptions/headers``. - public var headers: [String: String] { - _headers.dictionary - } + public let headers: HTTPFields struct MutableState { var listenForAuthEventsTask: Task? @@ -177,14 +174,14 @@ public final class SupabaseClient: Sendable { .authorization: "Bearer \(supabaseKey)", .apiKey: supabaseKey, ] - _headers = headers.merging(with: options.global.headers) + self.headers = options.global.headers.merging(headers) { $1 } // default storage key uses the supabase project ref as a namespace let defaultStorageKey = "sb-\(supabaseURL.host!.split(separator: ".")[0])-auth-token" _auth = AuthClient( url: supabaseURL.appendingPathComponent("/auth/v1"), - headers: _headers, + headers: self.headers, flowType: options.auth.flowType, redirectToURL: options.auth.redirectToURL, storageKey: options.auth.storageKey ?? defaultStorageKey, @@ -206,13 +203,13 @@ public final class SupabaseClient: Sendable { _realtime = UncheckedSendable( RealtimeClient( supabaseURL.appendingPathComponent("/realtime/v1").absoluteString, - headers: _headers, - params: _headers.dictionary + headers: headers, + params: headers.dictionary ) ) var realtimeOptions = options.realtime - realtimeOptions.headers.merge(with: _headers) + realtimeOptions.headers.merge(self.headers) { $1 } if realtimeOptions.logger == nil { realtimeOptions.logger = options.global.logger } diff --git a/Sources/TestHelpers/HTTPClientMock.swift b/Sources/TestHelpers/HTTPClientMock.swift index 3de790de..57a1402b 100644 --- a/Sources/TestHelpers/HTTPClientMock.swift +++ b/Sources/TestHelpers/HTTPClientMock.swift @@ -7,16 +7,18 @@ import ConcurrencyExtras import Foundation +import HTTPTypes import Helpers import XCTestDynamicOverlay package actor HTTPClientMock: HTTPClientType { + package struct MockNotFound: Error {} - private var mocks = [@Sendable (HTTPRequest) async throws -> HTTPResponse?]() + private var mocks = [@Sendable (HTTPRequest, Data?) async throws -> (Data, HTTPResponse)?]() /// Requests received by this client in order. - package var receivedRequests: [HTTPRequest] = [] + package var receivedRequests: [(HTTPRequest, Data?)] = [] /// Responses returned by this client in order. package var returnedResponses: [Result] = [] @@ -25,12 +27,12 @@ package actor HTTPClientMock: HTTPClientType { @discardableResult package func when( - _ request: @escaping @Sendable (HTTPRequest) -> Bool, - return response: @escaping @Sendable (HTTPRequest) async throws -> HTTPResponse + _ request: @escaping @Sendable (HTTPRequest, Data?) -> Bool, + return response: @escaping @Sendable (HTTPRequest, Data?) async throws -> (Data, HTTPResponse) ) -> Self { - mocks.append { r in - if request(r) { - return try await response(r) + mocks.append { r, b in + if request(r, b) { + return try await response(r, b) } return nil } @@ -39,19 +41,22 @@ package actor HTTPClientMock: HTTPClientType { @discardableResult package func any( - _ response: @escaping @Sendable (HTTPRequest) async throws -> HTTPResponse + _ response: @escaping @Sendable (HTTPRequest, Data?) async throws -> (Data, HTTPResponse) ) -> Self { - when({ _ in true }, return: response) + when({ _, _ in true }, return: response) } - package func send(_ request: HTTPRequest) async throws -> HTTPResponse { - receivedRequests.append(request) + package func send( + _ request: HTTPRequest, + _ bodyData: Data? + ) async throws -> (Data, HTTPResponse) { + receivedRequests.append((request, bodyData)) for mock in mocks { do { - if let response = try await mock(request) { + if let (data, response) = try await mock(request, bodyData) { returnedResponses.append(.success(response)) - return response + return (data, response) } } catch { returnedResponses.append(.failure(error)) diff --git a/Tests/AuthTests/AuthClientMultipleInstancesTests.swift b/Tests/AuthTests/AuthClientMultipleInstancesTests.swift index 26998388..fe4c3231 100644 --- a/Tests/AuthTests/AuthClientMultipleInstancesTests.swift +++ b/Tests/AuthTests/AuthClientMultipleInstancesTests.swift @@ -5,10 +5,11 @@ // Created by Guilherme Souza on 05/07/24. // -@testable import Auth import TestHelpers import XCTest +@testable import Auth + final class AuthClientMultipleInstancesTests: XCTestCase { func testMultipleAuthClientInstances() { let url = URL(string: "http://localhost:54321/auth")! diff --git a/Tests/AuthTests/AuthClientTests.swift b/Tests/AuthTests/AuthClientTests.swift index 17905696..9c4fd989 100644 --- a/Tests/AuthTests/AuthClientTests.swift +++ b/Tests/AuthTests/AuthClientTests.swift @@ -7,6 +7,7 @@ import ConcurrencyExtras import CustomDump +import HTTPTypes import InlineSnapshotTesting import TestHelpers import XCTest @@ -80,8 +81,8 @@ final class AuthClientTests: XCTestCase { } func testSignOut() async throws { - sut = makeSUT { _ in - .stub() + sut = makeSUT { _, _ in + TestStub.stub() } Dependencies[sut.clientID].sessionStorage.store(.validSession) @@ -109,8 +110,8 @@ final class AuthClientTests: XCTestCase { } func testSignOutWithOthersScopeShouldNotRemoveLocalSession() async throws { - sut = makeSUT { _ in - .stub() + sut = makeSUT { _, _ in + TestStub.stub() } Dependencies[sut.clientID].sessionStorage.store(.validSession) @@ -122,14 +123,12 @@ final class AuthClientTests: XCTestCase { } func testSignOutShouldRemoveSessionIfUserIsNotFound() async throws { - sut = makeSUT { _ in + sut = makeSUT { _, _ in throw AuthError.api( message: "", errorCode: .unknown, - underlyingData: Data(), - underlyingResponse: HTTPURLResponse( - url: URL(string: "http://localhost")!, statusCode: 404, httpVersion: nil, - headerFields: nil)! + data: Data(), + response: HTTPResponse(status: .init(code: 404)) ) } @@ -155,14 +154,12 @@ final class AuthClientTests: XCTestCase { } func testSignOutShouldRemoveSessionIfJWTIsInvalid() async throws { - sut = makeSUT { _ in + sut = makeSUT { _, _ in throw AuthError.api( message: "", errorCode: .invalidCredentials, - underlyingData: Data(), - underlyingResponse: HTTPURLResponse( - url: URL(string: "http://localhost")!, statusCode: 401, httpVersion: nil, - headerFields: nil)! + data: Data(), + response: HTTPResponse(status: .init(code: 401)) ) } @@ -188,14 +185,12 @@ final class AuthClientTests: XCTestCase { } func testSignOutShouldRemoveSessionIf403Returned() async throws { - sut = makeSUT { _ in + sut = makeSUT { _, _ in throw AuthError.api( message: "", errorCode: .invalidCredentials, - underlyingData: Data(), - underlyingResponse: HTTPURLResponse( - url: URL(string: "http://localhost")!, statusCode: 403, httpVersion: nil, - headerFields: nil)! + data: Data(), + response: HTTPResponse(status: .init(code: 403)) ) } @@ -223,8 +218,8 @@ final class AuthClientTests: XCTestCase { func testSignInAnonymously() async throws { let session = Session(fromMockNamed: "anonymous-sign-in-response") - let sut = makeSUT { _ in - .stub(fromFileName: "anonymous-sign-in-response") + let sut = makeSUT { _, _ in + TestStub.stub(fromFileName: "anonymous-sign-in-response") } let eventsTask = Task { @@ -243,8 +238,8 @@ final class AuthClientTests: XCTestCase { } func testSignInWithOAuth() async throws { - let sut = makeSUT { _ in - .stub(fromFileName: "session") + let sut = makeSUT { _, _ in + TestStub.stub(fromFileName: "session") } let eventsTask = Task { @@ -266,8 +261,8 @@ final class AuthClientTests: XCTestCase { } func testGetLinkIdentityURL() async throws { - let sut = makeSUT { _ in - .stub( + let sut = makeSUT { _, _ in + TestStub.stub( """ { "url" : "https://github.com/login/oauth/authorize?client_id=1234&redirect_to=com.supabase.swift-examples://&redirect_uri=http://127.0.0.1:54321/auth/v1/callback&response_type=code&scope=user:email&skip_http_redirect=true&state=jwt" @@ -295,8 +290,8 @@ final class AuthClientTests: XCTestCase { func testLinkIdentity() async throws { let url = "https://github.com/login/oauth/authorize?client_id=1234&redirect_to=com.supabase.swift-examples://&redirect_uri=http://127.0.0.1:54321/auth/v1/callback&response_type=code&scope=user:email&skip_http_redirect=true&state=jwt" - let sut = makeSUT { _ in - .stub( + let sut = makeSUT { _, _ in + TestStub.stub( """ { "url" : "\(url)" @@ -318,12 +313,12 @@ final class AuthClientTests: XCTestCase { } func testAdminListUsers() async throws { - let sut = makeSUT { _ in - .stub( + let sut = makeSUT { _, _ in + TestStub.stub( fromFileName: "list-users-response", headers: [ - "X-Total-Count": "669", - "Link": + .xTotalCount: "669", + .link: "; rel=\"next\", ; rel=\"last\"", ] ) @@ -336,12 +331,12 @@ final class AuthClientTests: XCTestCase { } func testAdminListUsers_noNextPage() async throws { - let sut = makeSUT { _ in - .stub( + let sut = makeSUT { _, _ in + TestStub.stub( fromFileName: "list-users-response", headers: [ - "X-Total-Count": "669", - "Link": "; rel=\"last\"", + .xTotalCount: "669", + .link: "; rel=\"last\"", ] ) } @@ -378,20 +373,20 @@ final class AuthClientTests: XCTestCase { } private func makeSUT( - fetch: ((URLRequest) async throws -> HTTPResponse)? = nil + fetch: ((HTTPRequest, Data?) async throws -> (Data, HTTPResponse))? = nil ) -> AuthClient { let configuration = AuthClient.Configuration( url: clientURL, - headers: ["Apikey": "dummy.api.key"], + headers: [.apiKey: "dummy.api.key"], localStorage: storage, logger: nil, - fetch: { request in + fetch: { request, body in guard let fetch else { throw UnimplementedError() } - let response = try await fetch(request) - return (response.data, response.underlyingResponse) + let (data, response) = try await fetch(request, body) + return (data, response) } ) @@ -401,52 +396,47 @@ final class AuthClientTests: XCTestCase { } } -extension HTTPResponse { +struct TestStub { static func stub( _ body: String = "", code: Int = 200, - headers: [String: String]? = nil - ) -> HTTPResponse { - HTTPResponse( - data: body.data(using: .utf8)!, - response: HTTPURLResponse( - url: clientURL, - statusCode: code, - httpVersion: nil, + headers: HTTPFields = [:] + ) -> (Data, HTTPResponse) { + ( + Data(body.utf8), + HTTPResponse( + status: .init(code: code), headerFields: headers - )! + ) ) } static func stub( fromFileName fileName: String, code: Int = 200, - headers: [String: String]? = nil - ) -> HTTPResponse { - HTTPResponse( - data: json(named: fileName), - response: HTTPURLResponse( - url: clientURL, - statusCode: code, - httpVersion: nil, + headers: HTTPFields = [:] + ) -> (Data, HTTPResponse) { + ( + json(named: fileName), + HTTPResponse( + status: .init(code: code), headerFields: headers - )! + ) ) } static func stub( _ value: some Encodable, code: Int = 200, - headers: [String: String]? = nil - ) -> HTTPResponse { - HTTPResponse( - data: try! AuthClient.Configuration.jsonEncoder.encode(value), - response: HTTPURLResponse( - url: clientURL, - statusCode: code, - httpVersion: nil, + headers: HTTPFields = [:] + ) -> (Data, HTTPResponse) { + ( + try! AuthClient.Configuration.jsonEncoder.encode(value), + HTTPResponse( + status: .init(code: code), headerFields: headers - )! + ) ) + } } diff --git a/Tests/AuthTests/AuthErrorTests.swift b/Tests/AuthTests/AuthErrorTests.swift index 66695263..782de4d4 100644 --- a/Tests/AuthTests/AuthErrorTests.swift +++ b/Tests/AuthTests/AuthErrorTests.swift @@ -5,9 +5,11 @@ // Created by Guilherme Souza on 29/08/24. // -@testable import Auth +import HTTPTypes import XCTest +@testable import Auth + #if canImport(FoundationNetworking) import FoundationNetworking #endif @@ -25,13 +27,17 @@ final class AuthErrorTests: XCTestCase { let api = AuthError.api( message: "API Error", errorCode: .emailConflictIdentityNotDeletable, - underlyingData: Data(), - underlyingResponse: HTTPURLResponse(url: URL(string: "http://localhost")!, statusCode: 400, httpVersion: nil, headerFields: nil)! + data: Data(), + response: HTTPResponse(status: .init(code: 400)) ) XCTAssertEqual(api.errorCode, .emailConflictIdentityNotDeletable) XCTAssertEqual(api.message, "API Error") - let pkceGrantCodeExchange = AuthError.pkceGrantCodeExchange(message: "PKCE failure", error: nil, code: nil) + let pkceGrantCodeExchange = AuthError.pkceGrantCodeExchange( + message: "PKCE failure", + error: nil, + code: nil + ) XCTAssertEqual(pkceGrantCodeExchange.errorCode, .unknown) XCTAssertEqual(pkceGrantCodeExchange.message, "PKCE failure") diff --git a/Tests/AuthTests/ExtractParamsTests.swift b/Tests/AuthTests/ExtractParamsTests.swift index 4ced833e..daacb291 100644 --- a/Tests/AuthTests/ExtractParamsTests.swift +++ b/Tests/AuthTests/ExtractParamsTests.swift @@ -5,9 +5,10 @@ // Created by Guilherme Souza on 23/12/23. // -@testable import Auth import XCTest +@testable import Auth + final class ExtractParamsTests: XCTestCase { func testExtractParamsInQuery() { let code = UUID().uuidString diff --git a/Tests/AuthTests/RequestsTests.swift b/Tests/AuthTests/RequestsTests.swift index a2b36c79..da3e66d0 100644 --- a/Tests/AuthTests/RequestsTests.swift +++ b/Tests/AuthTests/RequestsTests.swift @@ -5,13 +5,15 @@ // Created by Guilherme Souza on 07/10/23. // -@testable import Auth +import HTTPTypes import Helpers import InlineSnapshotTesting import SnapshotTesting import TestHelpers import XCTest +@testable import Auth + #if canImport(FoundationNetworking) import FoundationNetworking #endif @@ -140,10 +142,10 @@ final class RequestsTests: XCTestCase { #if !os(Linux) && !os(Windows) func testSessionFromURL() async throws { - let sut = makeSUT(fetch: { request in - let authorizationHeader = request.allHTTPHeaderFields?["Authorization"] + let sut = makeSUT(fetch: { request, bodyData in + let authorizationHeader = request.headerFields[.authorization] XCTAssertEqual(authorizationHeader, "bearer accesstoken") - return (json(named: "user"), HTTPURLResponse.stub()) + return (json(named: "user"), HTTPResponse(status: .ok)) }) let currentDate = Date() @@ -430,7 +432,12 @@ final class RequestsTests: XCTestCase { Dependencies[sut.clientID].sessionStorage.store(.validSession) await assert { - _ = try await sut.mfa.enroll(params: MFAEnrollParams(issuer: "supabase.com", friendlyName: "test")) + _ = try await sut.mfa.enroll( + params: MFAEnrollParams( + issuer: "supabase.com", + friendlyName: "test" + ) + ) } } @@ -480,7 +487,13 @@ final class RequestsTests: XCTestCase { Dependencies[sut.clientID].sessionStorage.store(.validSession) await assert { - _ = try await sut.mfa.verify(params: .init(factorId: "123", challengeId: "123", code: "123456")) + _ = try await sut.mfa.verify( + params: .init( + factorId: "123", + challengeId: "123", + code: "123456" + ) + ) } } @@ -516,20 +529,22 @@ final class RequestsTests: XCTestCase { let configuration = AuthClient.Configuration( url: clientURL, - headers: ["Apikey": "dummy.api.key", "X-Client-Info": "gotrue-swift/x.y.z"], + headers: [.apiKey: "dummy.api.key", .xClientInfo: "gotrue-swift/x.y.z"], flowType: flowType, localStorage: InMemoryLocalStorage(), logger: nil, encoder: encoder, - fetch: { request in + fetch: { request, bodyData in DispatchQueue.main.sync { + var request = URLRequest(httpRequest: request)! + request.httpBody = bodyData assertSnapshot( of: request, as: .curl, record: record, file: file, testName: testName, line: line ) } if let fetch { - return try await fetch(request) + return try await fetch(request, bodyData) } throw UnimplementedError() @@ -539,14 +554,3 @@ final class RequestsTests: XCTestCase { return AuthClient(configuration: configuration) } } - -extension HTTPURLResponse { - fileprivate static func stub(code: Int = 200) -> HTTPURLResponse { - HTTPURLResponse( - url: clientURL, - statusCode: code, - httpVersion: nil, - headerFields: nil - )! - } -} diff --git a/Tests/AuthTests/SessionManagerTests.swift b/Tests/AuthTests/SessionManagerTests.swift index 3d866055..36c6cb3b 100644 --- a/Tests/AuthTests/SessionManagerTests.swift +++ b/Tests/AuthTests/SessionManagerTests.swift @@ -82,11 +82,13 @@ final class SessionManagerTests: XCTestCase { let (refreshSessionStream, refreshSessionContinuation) = AsyncStream.makeStream() await http.when( - { $0.url.path.contains("/token") }, - return: { _ in + { request, bodyData in + request.url!.path.contains("/token") + }, + return: { _, _ in refreshSessionCallCount.withValue { $0 += 1 } let session = await refreshSessionStream.first(where: { _ in true })! - return .stub(session) + return TestStub.stub(session) } ) diff --git a/Tests/AuthTests/StoredSessionTests.swift b/Tests/AuthTests/StoredSessionTests.swift index 9e466ec2..47a3592d 100644 --- a/Tests/AuthTests/StoredSessionTests.swift +++ b/Tests/AuthTests/StoredSessionTests.swift @@ -1,9 +1,10 @@ -@testable import Auth import ConcurrencyExtras import SnapshotTesting import TestHelpers import XCTest +@testable import Auth + final class StoredSessionTests: XCTestCase { let clientID = AuthClientID() @@ -37,11 +38,11 @@ final class StoredSessionTests: XCTestCase { appMetadata: [ "provider": "email", "providers": [ - "email", + "email" ], ], userMetadata: [ - "referrer_id": nil, + "referrer_id": nil ], aud: "authenticated", confirmationSentAt: ISO8601DateFormatter().date(from: "2022-04-09T11:57:01Z")!, @@ -65,13 +66,13 @@ final class StoredSessionTests: XCTestCase { identityId: UUID(uuidString: "859F402D-B3DE-4105-A1B9-932836D9193B")!, userId: UUID(uuidString: "859F402D-B3DE-4105-A1B9-932836D9193B")!, identityData: [ - "sub": "859f402d-b3de-4105-a1b9-932836d9193b", + "sub": "859f402d-b3de-4105-a1b9-932836d9193b" ], provider: "email", createdAt: ISO8601DateFormatter().date(from: "2022-04-09T11:57:01Z")!, lastSignInAt: ISO8601DateFormatter().date(from: "2022-04-09T11:57:01Z")!, updatedAt: ISO8601DateFormatter().date(from: "2022-04-09T11:57:01Z")! - ), + ) ], factors: nil ) diff --git a/Tests/AuthTests/__Snapshots__/RequestsTests/testDeleteUser.1.txt b/Tests/AuthTests/__Snapshots__/RequestsTests/testDeleteUser.1.txt index 2e6dd0e4..650a5fa9 100644 --- a/Tests/AuthTests/__Snapshots__/RequestsTests/testDeleteUser.1.txt +++ b/Tests/AuthTests/__Snapshots__/RequestsTests/testDeleteUser.1.txt @@ -1,8 +1,8 @@ curl \ --request DELETE \ - --header "Apikey: dummy.api.key" \ --header "Content-Type: application/json" \ --header "X-Client-Info: gotrue-swift/x.y.z" \ --header "X-Supabase-Api-Version: 2024-01-01" \ + --header "apiKey: dummy.api.key" \ --data "{\"should_soft_delete\":false}" \ "http://localhost:54321/auth/v1/admin/users/E621E1F8-C36C-495A-93FC-0C247A3E6E5F" \ No newline at end of file diff --git a/Tests/AuthTests/__Snapshots__/RequestsTests/testGetLinkIdentityURL.1.txt b/Tests/AuthTests/__Snapshots__/RequestsTests/testGetLinkIdentityURL.1.txt index 44ad6259..80a2c5d3 100644 --- a/Tests/AuthTests/__Snapshots__/RequestsTests/testGetLinkIdentityURL.1.txt +++ b/Tests/AuthTests/__Snapshots__/RequestsTests/testGetLinkIdentityURL.1.txt @@ -1,6 +1,6 @@ curl \ - --header "Apikey: dummy.api.key" \ --header "Authorization: Bearer accesstoken" \ --header "X-Client-Info: gotrue-swift/x.y.z" \ --header "X-Supabase-Api-Version: 2024-01-01" \ + --header "apiKey: dummy.api.key" \ "http://localhost:54321/auth/v1/user/identities/authorize?extra_key=extra_value&provider=github&redirect_to=https://supabase.com&scopes=user:email&skip_http_redirect=true" \ No newline at end of file diff --git a/Tests/AuthTests/__Snapshots__/RequestsTests/testMFAChallenge.1.txt b/Tests/AuthTests/__Snapshots__/RequestsTests/testMFAChallenge.1.txt index e0263d44..64fa5cc1 100644 --- a/Tests/AuthTests/__Snapshots__/RequestsTests/testMFAChallenge.1.txt +++ b/Tests/AuthTests/__Snapshots__/RequestsTests/testMFAChallenge.1.txt @@ -1,7 +1,7 @@ curl \ --request POST \ - --header "Apikey: dummy.api.key" \ --header "Authorization: Bearer accesstoken" \ --header "X-Client-Info: gotrue-swift/x.y.z" \ --header "X-Supabase-Api-Version: 2024-01-01" \ + --header "apiKey: dummy.api.key" \ "http://localhost:54321/auth/v1/factors/123/challenge" \ No newline at end of file diff --git a/Tests/AuthTests/__Snapshots__/RequestsTests/testMFAChallengePhone.1.txt b/Tests/AuthTests/__Snapshots__/RequestsTests/testMFAChallengePhone.1.txt index 296f1940..12fbbfbd 100644 --- a/Tests/AuthTests/__Snapshots__/RequestsTests/testMFAChallengePhone.1.txt +++ b/Tests/AuthTests/__Snapshots__/RequestsTests/testMFAChallengePhone.1.txt @@ -1,9 +1,9 @@ curl \ --request POST \ - --header "Apikey: dummy.api.key" \ --header "Authorization: Bearer accesstoken" \ --header "Content-Type: application/json" \ --header "X-Client-Info: gotrue-swift/x.y.z" \ --header "X-Supabase-Api-Version: 2024-01-01" \ + --header "apiKey: dummy.api.key" \ --data "{\"channel\":\"whatsapp\"}" \ "http://localhost:54321/auth/v1/factors/123/challenge" \ No newline at end of file diff --git a/Tests/AuthTests/__Snapshots__/RequestsTests/testMFAEnrollLegacy.1.txt b/Tests/AuthTests/__Snapshots__/RequestsTests/testMFAEnrollLegacy.1.txt index 51778779..52c874b2 100644 --- a/Tests/AuthTests/__Snapshots__/RequestsTests/testMFAEnrollLegacy.1.txt +++ b/Tests/AuthTests/__Snapshots__/RequestsTests/testMFAEnrollLegacy.1.txt @@ -1,9 +1,9 @@ curl \ --request POST \ - --header "Apikey: dummy.api.key" \ --header "Authorization: Bearer accesstoken" \ --header "Content-Type: application/json" \ --header "X-Client-Info: gotrue-swift/x.y.z" \ --header "X-Supabase-Api-Version: 2024-01-01" \ + --header "apiKey: dummy.api.key" \ --data "{\"factor_type\":\"totp\",\"friendly_name\":\"test\",\"issuer\":\"supabase.com\"}" \ "http://localhost:54321/auth/v1/factors" \ No newline at end of file diff --git a/Tests/AuthTests/__Snapshots__/RequestsTests/testMFAEnrollPhone.1.txt b/Tests/AuthTests/__Snapshots__/RequestsTests/testMFAEnrollPhone.1.txt index 5ae9482b..0b311c6b 100644 --- a/Tests/AuthTests/__Snapshots__/RequestsTests/testMFAEnrollPhone.1.txt +++ b/Tests/AuthTests/__Snapshots__/RequestsTests/testMFAEnrollPhone.1.txt @@ -1,9 +1,9 @@ curl \ --request POST \ - --header "Apikey: dummy.api.key" \ --header "Authorization: Bearer accesstoken" \ --header "Content-Type: application/json" \ --header "X-Client-Info: gotrue-swift/x.y.z" \ --header "X-Supabase-Api-Version: 2024-01-01" \ + --header "apiKey: dummy.api.key" \ --data "{\"factor_type\":\"phone\",\"friendly_name\":\"test\",\"phone\":\"+1 202-918-2132\"}" \ "http://localhost:54321/auth/v1/factors" \ No newline at end of file diff --git a/Tests/AuthTests/__Snapshots__/RequestsTests/testMFAEnrollTotp.1.txt b/Tests/AuthTests/__Snapshots__/RequestsTests/testMFAEnrollTotp.1.txt index 51778779..52c874b2 100644 --- a/Tests/AuthTests/__Snapshots__/RequestsTests/testMFAEnrollTotp.1.txt +++ b/Tests/AuthTests/__Snapshots__/RequestsTests/testMFAEnrollTotp.1.txt @@ -1,9 +1,9 @@ curl \ --request POST \ - --header "Apikey: dummy.api.key" \ --header "Authorization: Bearer accesstoken" \ --header "Content-Type: application/json" \ --header "X-Client-Info: gotrue-swift/x.y.z" \ --header "X-Supabase-Api-Version: 2024-01-01" \ + --header "apiKey: dummy.api.key" \ --data "{\"factor_type\":\"totp\",\"friendly_name\":\"test\",\"issuer\":\"supabase.com\"}" \ "http://localhost:54321/auth/v1/factors" \ No newline at end of file diff --git a/Tests/AuthTests/__Snapshots__/RequestsTests/testMFAUnenroll.1.txt b/Tests/AuthTests/__Snapshots__/RequestsTests/testMFAUnenroll.1.txt index 75c28574..6b0764cc 100644 --- a/Tests/AuthTests/__Snapshots__/RequestsTests/testMFAUnenroll.1.txt +++ b/Tests/AuthTests/__Snapshots__/RequestsTests/testMFAUnenroll.1.txt @@ -1,7 +1,7 @@ curl \ --request DELETE \ - --header "Apikey: dummy.api.key" \ --header "Authorization: Bearer accesstoken" \ --header "X-Client-Info: gotrue-swift/x.y.z" \ --header "X-Supabase-Api-Version: 2024-01-01" \ + --header "apiKey: dummy.api.key" \ "http://localhost:54321/auth/v1/factors/123" \ No newline at end of file diff --git a/Tests/AuthTests/__Snapshots__/RequestsTests/testMFAVerify.1.txt b/Tests/AuthTests/__Snapshots__/RequestsTests/testMFAVerify.1.txt index b336ccaf..e08455b4 100644 --- a/Tests/AuthTests/__Snapshots__/RequestsTests/testMFAVerify.1.txt +++ b/Tests/AuthTests/__Snapshots__/RequestsTests/testMFAVerify.1.txt @@ -1,9 +1,9 @@ curl \ --request POST \ - --header "Apikey: dummy.api.key" \ --header "Authorization: Bearer accesstoken" \ --header "Content-Type: application/json" \ --header "X-Client-Info: gotrue-swift/x.y.z" \ --header "X-Supabase-Api-Version: 2024-01-01" \ + --header "apiKey: dummy.api.key" \ --data "{\"challenge_id\":\"123\",\"code\":\"123456\",\"factor_id\":\"123\"}" \ "http://localhost:54321/auth/v1/factors/123/verify" \ No newline at end of file diff --git a/Tests/AuthTests/__Snapshots__/RequestsTests/testReauthenticate.1.txt b/Tests/AuthTests/__Snapshots__/RequestsTests/testReauthenticate.1.txt index d132361a..c03004a7 100644 --- a/Tests/AuthTests/__Snapshots__/RequestsTests/testReauthenticate.1.txt +++ b/Tests/AuthTests/__Snapshots__/RequestsTests/testReauthenticate.1.txt @@ -1,6 +1,6 @@ curl \ - --header "Apikey: dummy.api.key" \ --header "Authorization: Bearer accesstoken" \ --header "X-Client-Info: gotrue-swift/x.y.z" \ --header "X-Supabase-Api-Version: 2024-01-01" \ + --header "apiKey: dummy.api.key" \ "http://localhost:54321/auth/v1/reauthenticate" \ No newline at end of file diff --git a/Tests/AuthTests/__Snapshots__/RequestsTests/testRefreshSession.1.txt b/Tests/AuthTests/__Snapshots__/RequestsTests/testRefreshSession.1.txt index 69f5f61c..383a9e5a 100644 --- a/Tests/AuthTests/__Snapshots__/RequestsTests/testRefreshSession.1.txt +++ b/Tests/AuthTests/__Snapshots__/RequestsTests/testRefreshSession.1.txt @@ -1,8 +1,8 @@ curl \ --request POST \ - --header "Apikey: dummy.api.key" \ --header "Content-Type: application/json" \ --header "X-Client-Info: gotrue-swift/x.y.z" \ --header "X-Supabase-Api-Version: 2024-01-01" \ + --header "apiKey: dummy.api.key" \ --data "{\"refresh_token\":\"refresh-token\"}" \ "http://localhost:54321/auth/v1/token?grant_type=refresh_token" \ No newline at end of file diff --git a/Tests/AuthTests/__Snapshots__/RequestsTests/testResendEmail.1.txt b/Tests/AuthTests/__Snapshots__/RequestsTests/testResendEmail.1.txt index 8f195886..5ece3cce 100644 --- a/Tests/AuthTests/__Snapshots__/RequestsTests/testResendEmail.1.txt +++ b/Tests/AuthTests/__Snapshots__/RequestsTests/testResendEmail.1.txt @@ -1,8 +1,8 @@ curl \ --request POST \ - --header "Apikey: dummy.api.key" \ --header "Content-Type: application/json" \ --header "X-Client-Info: gotrue-swift/x.y.z" \ --header "X-Supabase-Api-Version: 2024-01-01" \ + --header "apiKey: dummy.api.key" \ --data "{\"email\":\"example@mail.com\",\"gotrue_meta_security\":{\"captcha_token\":\"captcha-token\"},\"type\":\"email_change\"}" \ "http://localhost:54321/auth/v1/resend?redirect_to=https://supabase.com" \ No newline at end of file diff --git a/Tests/AuthTests/__Snapshots__/RequestsTests/testResendPhone.1.txt b/Tests/AuthTests/__Snapshots__/RequestsTests/testResendPhone.1.txt index 16f203b3..5e2c9c47 100644 --- a/Tests/AuthTests/__Snapshots__/RequestsTests/testResendPhone.1.txt +++ b/Tests/AuthTests/__Snapshots__/RequestsTests/testResendPhone.1.txt @@ -1,8 +1,8 @@ curl \ --request POST \ - --header "Apikey: dummy.api.key" \ --header "Content-Type: application/json" \ --header "X-Client-Info: gotrue-swift/x.y.z" \ --header "X-Supabase-Api-Version: 2024-01-01" \ + --header "apiKey: dummy.api.key" \ --data "{\"gotrue_meta_security\":{\"captcha_token\":\"captcha-token\"},\"phone\":\"+1 202-918-2132\",\"type\":\"phone_change\"}" \ "http://localhost:54321/auth/v1/resend" \ No newline at end of file diff --git a/Tests/AuthTests/__Snapshots__/RequestsTests/testResetPasswordForEmail.1.txt b/Tests/AuthTests/__Snapshots__/RequestsTests/testResetPasswordForEmail.1.txt index c77f72d0..6d9ae8a6 100644 --- a/Tests/AuthTests/__Snapshots__/RequestsTests/testResetPasswordForEmail.1.txt +++ b/Tests/AuthTests/__Snapshots__/RequestsTests/testResetPasswordForEmail.1.txt @@ -1,8 +1,8 @@ curl \ --request POST \ - --header "Apikey: dummy.api.key" \ --header "Content-Type: application/json" \ --header "X-Client-Info: gotrue-swift/x.y.z" \ --header "X-Supabase-Api-Version: 2024-01-01" \ + --header "apiKey: dummy.api.key" \ --data "{\"email\":\"example@mail.com\",\"gotrue_meta_security\":{\"captcha_token\":\"captcha-token\"}}" \ "http://localhost:54321/auth/v1/recover?redirect_to=https://supabase.com" \ No newline at end of file diff --git a/Tests/AuthTests/__Snapshots__/RequestsTests/testSessionFromURL.1.txt b/Tests/AuthTests/__Snapshots__/RequestsTests/testSessionFromURL.1.txt index ca2aed24..a38ebb8a 100644 --- a/Tests/AuthTests/__Snapshots__/RequestsTests/testSessionFromURL.1.txt +++ b/Tests/AuthTests/__Snapshots__/RequestsTests/testSessionFromURL.1.txt @@ -1,6 +1,6 @@ curl \ - --header "Apikey: dummy.api.key" \ --header "Authorization: bearer accesstoken" \ --header "X-Client-Info: gotrue-swift/x.y.z" \ --header "X-Supabase-Api-Version: 2024-01-01" \ + --header "apiKey: dummy.api.key" \ "http://localhost:54321/auth/v1/user" \ No newline at end of file diff --git a/Tests/AuthTests/__Snapshots__/RequestsTests/testSetSessionWithAExpiredToken.1.txt b/Tests/AuthTests/__Snapshots__/RequestsTests/testSetSessionWithAExpiredToken.1.txt index 70612d26..81ad7be9 100644 --- a/Tests/AuthTests/__Snapshots__/RequestsTests/testSetSessionWithAExpiredToken.1.txt +++ b/Tests/AuthTests/__Snapshots__/RequestsTests/testSetSessionWithAExpiredToken.1.txt @@ -1,8 +1,8 @@ curl \ --request POST \ - --header "Apikey: dummy.api.key" \ --header "Content-Type: application/json" \ --header "X-Client-Info: gotrue-swift/x.y.z" \ --header "X-Supabase-Api-Version: 2024-01-01" \ + --header "apiKey: dummy.api.key" \ --data "{\"refresh_token\":\"dummy-refresh-token\"}" \ "http://localhost:54321/auth/v1/token?grant_type=refresh_token" \ No newline at end of file diff --git a/Tests/AuthTests/__Snapshots__/RequestsTests/testSetSessionWithAFutureExpirationDate.1.txt b/Tests/AuthTests/__Snapshots__/RequestsTests/testSetSessionWithAFutureExpirationDate.1.txt index d8d24073..0532048c 100644 --- a/Tests/AuthTests/__Snapshots__/RequestsTests/testSetSessionWithAFutureExpirationDate.1.txt +++ b/Tests/AuthTests/__Snapshots__/RequestsTests/testSetSessionWithAFutureExpirationDate.1.txt @@ -1,6 +1,6 @@ curl \ - --header "Apikey: dummy.api.key" \ --header "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJhdXRoZW50aWNhdGVkIiwiZXhwIjo0ODUyMTYzNTkzLCJzdWIiOiJmMzNkM2VjOS1hMmVlLTQ3YzQtODBlMS01YmQ5MTlmM2Q4YjgiLCJlbWFpbCI6ImhpQGJpbmFyeXNjcmFwaW5nLmNvIiwicGhvbmUiOiIiLCJhcHBfbWV0YWRhdGEiOnsicHJvdmlkZXIiOiJlbWFpbCIsInByb3ZpZGVycyI6WyJlbWFpbCJdfSwidXNlcl9tZXRhZGF0YSI6e30sInJvbGUiOiJhdXRoZW50aWNhdGVkIn0.UiEhoahP9GNrBKw_OHBWyqYudtoIlZGkrjs7Qa8hU7I" \ --header "X-Client-Info: gotrue-swift/x.y.z" \ --header "X-Supabase-Api-Version: 2024-01-01" \ + --header "apiKey: dummy.api.key" \ "http://localhost:54321/auth/v1/user" \ No newline at end of file diff --git a/Tests/AuthTests/__Snapshots__/RequestsTests/testSignInAnonymously.1.txt b/Tests/AuthTests/__Snapshots__/RequestsTests/testSignInAnonymously.1.txt index 5fa3cf34..782dea84 100644 --- a/Tests/AuthTests/__Snapshots__/RequestsTests/testSignInAnonymously.1.txt +++ b/Tests/AuthTests/__Snapshots__/RequestsTests/testSignInAnonymously.1.txt @@ -1,8 +1,8 @@ curl \ --request POST \ - --header "Apikey: dummy.api.key" \ --header "Content-Type: application/json" \ --header "X-Client-Info: gotrue-swift/x.y.z" \ --header "X-Supabase-Api-Version: 2024-01-01" \ + --header "apiKey: dummy.api.key" \ --data "{\"data\":{\"custom_key\":\"custom_value\"},\"gotrue_meta_security\":{\"captcha_token\":\"captcha-token\"}}" \ "http://localhost:54321/auth/v1/signup" \ No newline at end of file diff --git a/Tests/AuthTests/__Snapshots__/RequestsTests/testSignInWithEmailAndPassword.1.txt b/Tests/AuthTests/__Snapshots__/RequestsTests/testSignInWithEmailAndPassword.1.txt index df2dfbbf..41ba1e1c 100644 --- a/Tests/AuthTests/__Snapshots__/RequestsTests/testSignInWithEmailAndPassword.1.txt +++ b/Tests/AuthTests/__Snapshots__/RequestsTests/testSignInWithEmailAndPassword.1.txt @@ -1,8 +1,8 @@ curl \ --request POST \ - --header "Apikey: dummy.api.key" \ --header "Content-Type: application/json" \ --header "X-Client-Info: gotrue-swift/x.y.z" \ --header "X-Supabase-Api-Version: 2024-01-01" \ + --header "apiKey: dummy.api.key" \ --data "{\"email\":\"example@mail.com\",\"gotrue_meta_security\":{\"captcha_token\":\"dummy-captcha\"},\"password\":\"the.pass\"}" \ "http://localhost:54321/auth/v1/token?grant_type=password" \ No newline at end of file diff --git a/Tests/AuthTests/__Snapshots__/RequestsTests/testSignInWithIdToken.1.txt b/Tests/AuthTests/__Snapshots__/RequestsTests/testSignInWithIdToken.1.txt index 37477c44..af1c88b7 100644 --- a/Tests/AuthTests/__Snapshots__/RequestsTests/testSignInWithIdToken.1.txt +++ b/Tests/AuthTests/__Snapshots__/RequestsTests/testSignInWithIdToken.1.txt @@ -1,8 +1,8 @@ curl \ --request POST \ - --header "Apikey: dummy.api.key" \ --header "Content-Type: application/json" \ --header "X-Client-Info: gotrue-swift/x.y.z" \ --header "X-Supabase-Api-Version: 2024-01-01" \ + --header "apiKey: dummy.api.key" \ --data "{\"access_token\":\"access-token\",\"gotrue_meta_security\":{\"captcha_token\":\"captcha-token\"},\"id_token\":\"id-token\",\"nonce\":\"nonce\",\"provider\":\"apple\"}" \ "http://localhost:54321/auth/v1/token?grant_type=id_token" \ No newline at end of file diff --git a/Tests/AuthTests/__Snapshots__/RequestsTests/testSignInWithOTPUsingEmail.1.txt b/Tests/AuthTests/__Snapshots__/RequestsTests/testSignInWithOTPUsingEmail.1.txt index 19c1ceba..727004e6 100644 --- a/Tests/AuthTests/__Snapshots__/RequestsTests/testSignInWithOTPUsingEmail.1.txt +++ b/Tests/AuthTests/__Snapshots__/RequestsTests/testSignInWithOTPUsingEmail.1.txt @@ -1,8 +1,8 @@ curl \ --request POST \ - --header "Apikey: dummy.api.key" \ --header "Content-Type: application/json" \ --header "X-Client-Info: gotrue-swift/x.y.z" \ --header "X-Supabase-Api-Version: 2024-01-01" \ + --header "apiKey: dummy.api.key" \ --data "{\"create_user\":true,\"data\":{\"custom_key\":\"custom_value\"},\"email\":\"example@mail.com\",\"gotrue_meta_security\":{\"captcha_token\":\"dummy-captcha\"}}" \ "http://localhost:54321/auth/v1/otp?redirect_to=https://supabase.com" \ No newline at end of file diff --git a/Tests/AuthTests/__Snapshots__/RequestsTests/testSignInWithOTPUsingPhone.1.txt b/Tests/AuthTests/__Snapshots__/RequestsTests/testSignInWithOTPUsingPhone.1.txt index 512cbccc..b34950d9 100644 --- a/Tests/AuthTests/__Snapshots__/RequestsTests/testSignInWithOTPUsingPhone.1.txt +++ b/Tests/AuthTests/__Snapshots__/RequestsTests/testSignInWithOTPUsingPhone.1.txt @@ -1,8 +1,8 @@ curl \ --request POST \ - --header "Apikey: dummy.api.key" \ --header "Content-Type: application/json" \ --header "X-Client-Info: gotrue-swift/x.y.z" \ --header "X-Supabase-Api-Version: 2024-01-01" \ + --header "apiKey: dummy.api.key" \ --data "{\"channel\":\"sms\",\"create_user\":true,\"data\":{\"custom_key\":\"custom_value\"},\"gotrue_meta_security\":{\"captcha_token\":\"dummy-captcha\"},\"phone\":\"+1 202-918-2132\"}" \ "http://localhost:54321/auth/v1/otp" \ No newline at end of file diff --git a/Tests/AuthTests/__Snapshots__/RequestsTests/testSignInWithPhoneAndPassword.1.txt b/Tests/AuthTests/__Snapshots__/RequestsTests/testSignInWithPhoneAndPassword.1.txt index c29e1e69..cb7a24ff 100644 --- a/Tests/AuthTests/__Snapshots__/RequestsTests/testSignInWithPhoneAndPassword.1.txt +++ b/Tests/AuthTests/__Snapshots__/RequestsTests/testSignInWithPhoneAndPassword.1.txt @@ -1,8 +1,8 @@ curl \ --request POST \ - --header "Apikey: dummy.api.key" \ --header "Content-Type: application/json" \ --header "X-Client-Info: gotrue-swift/x.y.z" \ --header "X-Supabase-Api-Version: 2024-01-01" \ + --header "apiKey: dummy.api.key" \ --data "{\"gotrue_meta_security\":{\"captcha_token\":\"dummy-captcha\"},\"password\":\"the.pass\",\"phone\":\"+1 202-918-2132\"}" \ "http://localhost:54321/auth/v1/token?grant_type=password" \ No newline at end of file diff --git a/Tests/AuthTests/__Snapshots__/RequestsTests/testSignInWithSSOUsingDomain.1.txt b/Tests/AuthTests/__Snapshots__/RequestsTests/testSignInWithSSOUsingDomain.1.txt index 1726c3ce..711b550d 100644 --- a/Tests/AuthTests/__Snapshots__/RequestsTests/testSignInWithSSOUsingDomain.1.txt +++ b/Tests/AuthTests/__Snapshots__/RequestsTests/testSignInWithSSOUsingDomain.1.txt @@ -1,8 +1,8 @@ curl \ --request POST \ - --header "Apikey: dummy.api.key" \ --header "Content-Type: application/json" \ --header "X-Client-Info: gotrue-swift/x.y.z" \ --header "X-Supabase-Api-Version: 2024-01-01" \ + --header "apiKey: dummy.api.key" \ --data "{\"domain\":\"supabase.com\",\"gotrue_meta_security\":{\"captcha_token\":\"captcha-token\"},\"redirect_to\":\"https:\/\/supabase.com\"}" \ "http://localhost:54321/auth/v1/sso" \ No newline at end of file diff --git a/Tests/AuthTests/__Snapshots__/RequestsTests/testSignInWithSSOUsingProviderId.1.txt b/Tests/AuthTests/__Snapshots__/RequestsTests/testSignInWithSSOUsingProviderId.1.txt index 8674ed09..5b16b3bf 100644 --- a/Tests/AuthTests/__Snapshots__/RequestsTests/testSignInWithSSOUsingProviderId.1.txt +++ b/Tests/AuthTests/__Snapshots__/RequestsTests/testSignInWithSSOUsingProviderId.1.txt @@ -1,8 +1,8 @@ curl \ --request POST \ - --header "Apikey: dummy.api.key" \ --header "Content-Type: application/json" \ --header "X-Client-Info: gotrue-swift/x.y.z" \ --header "X-Supabase-Api-Version: 2024-01-01" \ + --header "apiKey: dummy.api.key" \ --data "{\"gotrue_meta_security\":{\"captcha_token\":\"captcha-token\"},\"provider_id\":\"E621E1F8-C36C-495A-93FC-0C247A3E6E5F\",\"redirect_to\":\"https:\/\/supabase.com\"}" \ "http://localhost:54321/auth/v1/sso" \ No newline at end of file diff --git a/Tests/AuthTests/__Snapshots__/RequestsTests/testSignOut.1.txt b/Tests/AuthTests/__Snapshots__/RequestsTests/testSignOut.1.txt index 1868f0c5..69649604 100644 --- a/Tests/AuthTests/__Snapshots__/RequestsTests/testSignOut.1.txt +++ b/Tests/AuthTests/__Snapshots__/RequestsTests/testSignOut.1.txt @@ -1,7 +1,7 @@ curl \ --request POST \ - --header "Apikey: dummy.api.key" \ --header "Authorization: Bearer accesstoken" \ --header "X-Client-Info: gotrue-swift/x.y.z" \ --header "X-Supabase-Api-Version: 2024-01-01" \ + --header "apiKey: dummy.api.key" \ "http://localhost:54321/auth/v1/logout?scope=global" \ No newline at end of file diff --git a/Tests/AuthTests/__Snapshots__/RequestsTests/testSignOutWithLocalScope.1.txt b/Tests/AuthTests/__Snapshots__/RequestsTests/testSignOutWithLocalScope.1.txt index 151ef1ce..3cedf642 100644 --- a/Tests/AuthTests/__Snapshots__/RequestsTests/testSignOutWithLocalScope.1.txt +++ b/Tests/AuthTests/__Snapshots__/RequestsTests/testSignOutWithLocalScope.1.txt @@ -1,7 +1,7 @@ curl \ --request POST \ - --header "Apikey: dummy.api.key" \ --header "Authorization: Bearer accesstoken" \ --header "X-Client-Info: gotrue-swift/x.y.z" \ --header "X-Supabase-Api-Version: 2024-01-01" \ + --header "apiKey: dummy.api.key" \ "http://localhost:54321/auth/v1/logout?scope=local" \ No newline at end of file diff --git a/Tests/AuthTests/__Snapshots__/RequestsTests/testSignOutWithOthersScope.1.txt b/Tests/AuthTests/__Snapshots__/RequestsTests/testSignOutWithOthersScope.1.txt index 44cf10c9..ad5d0597 100644 --- a/Tests/AuthTests/__Snapshots__/RequestsTests/testSignOutWithOthersScope.1.txt +++ b/Tests/AuthTests/__Snapshots__/RequestsTests/testSignOutWithOthersScope.1.txt @@ -1,7 +1,7 @@ curl \ --request POST \ - --header "Apikey: dummy.api.key" \ --header "Authorization: Bearer accesstoken" \ --header "X-Client-Info: gotrue-swift/x.y.z" \ --header "X-Supabase-Api-Version: 2024-01-01" \ + --header "apiKey: dummy.api.key" \ "http://localhost:54321/auth/v1/logout?scope=others" \ No newline at end of file diff --git a/Tests/AuthTests/__Snapshots__/RequestsTests/testSignUpWithEmailAndPassword.1.txt b/Tests/AuthTests/__Snapshots__/RequestsTests/testSignUpWithEmailAndPassword.1.txt index 1f3cf59e..dd8ed048 100644 --- a/Tests/AuthTests/__Snapshots__/RequestsTests/testSignUpWithEmailAndPassword.1.txt +++ b/Tests/AuthTests/__Snapshots__/RequestsTests/testSignUpWithEmailAndPassword.1.txt @@ -1,8 +1,8 @@ curl \ --request POST \ - --header "Apikey: dummy.api.key" \ --header "Content-Type: application/json" \ --header "X-Client-Info: gotrue-swift/x.y.z" \ --header "X-Supabase-Api-Version: 2024-01-01" \ + --header "apiKey: dummy.api.key" \ --data "{\"data\":{\"custom_key\":\"custom_value\"},\"email\":\"example@mail.com\",\"gotrue_meta_security\":{\"captcha_token\":\"dummy-captcha\"},\"password\":\"the.pass\"}" \ "http://localhost:54321/auth/v1/signup?redirect_to=https://supabase.com" \ No newline at end of file diff --git a/Tests/AuthTests/__Snapshots__/RequestsTests/testSignUpWithPhoneAndPassword.1.txt b/Tests/AuthTests/__Snapshots__/RequestsTests/testSignUpWithPhoneAndPassword.1.txt index 7bcbc227..d57109d2 100644 --- a/Tests/AuthTests/__Snapshots__/RequestsTests/testSignUpWithPhoneAndPassword.1.txt +++ b/Tests/AuthTests/__Snapshots__/RequestsTests/testSignUpWithPhoneAndPassword.1.txt @@ -1,8 +1,8 @@ curl \ --request POST \ - --header "Apikey: dummy.api.key" \ --header "Content-Type: application/json" \ --header "X-Client-Info: gotrue-swift/x.y.z" \ --header "X-Supabase-Api-Version: 2024-01-01" \ + --header "apiKey: dummy.api.key" \ --data "{\"data\":{\"custom_key\":\"custom_value\"},\"gotrue_meta_security\":{\"captcha_token\":\"dummy-captcha\"},\"password\":\"the.pass\",\"phone\":\"+1 202-918-2132\"}" \ "http://localhost:54321/auth/v1/signup" \ No newline at end of file diff --git a/Tests/AuthTests/__Snapshots__/RequestsTests/testUnlinkIdentity.1.txt b/Tests/AuthTests/__Snapshots__/RequestsTests/testUnlinkIdentity.1.txt index cc81e321..cf6f4001 100644 --- a/Tests/AuthTests/__Snapshots__/RequestsTests/testUnlinkIdentity.1.txt +++ b/Tests/AuthTests/__Snapshots__/RequestsTests/testUnlinkIdentity.1.txt @@ -1,7 +1,7 @@ curl \ --request DELETE \ - --header "Apikey: dummy.api.key" \ --header "Authorization: Bearer accesstoken" \ --header "X-Client-Info: gotrue-swift/x.y.z" \ --header "X-Supabase-Api-Version: 2024-01-01" \ + --header "apiKey: dummy.api.key" \ "http://localhost:54321/auth/v1/user/identities/E621E1F8-C36C-495A-93FC-0C247A3E6E5F" \ No newline at end of file diff --git a/Tests/AuthTests/__Snapshots__/RequestsTests/testUpdateUser.1.txt b/Tests/AuthTests/__Snapshots__/RequestsTests/testUpdateUser.1.txt index 45eaa5f0..5e61a0d9 100644 --- a/Tests/AuthTests/__Snapshots__/RequestsTests/testUpdateUser.1.txt +++ b/Tests/AuthTests/__Snapshots__/RequestsTests/testUpdateUser.1.txt @@ -1,9 +1,9 @@ curl \ --request PUT \ - --header "Apikey: dummy.api.key" \ --header "Authorization: Bearer accesstoken" \ --header "Content-Type: application/json" \ --header "X-Client-Info: gotrue-swift/x.y.z" \ --header "X-Supabase-Api-Version: 2024-01-01" \ + --header "apiKey: dummy.api.key" \ --data "{\"data\":{\"custom_key\":\"custom_value\"},\"email\":\"example@mail.com\",\"email_change_token\":\"123456\",\"nonce\":\"abcdef\",\"password\":\"another.pass\",\"phone\":\"+1 202-918-2132\"}" \ "http://localhost:54321/auth/v1/user" \ No newline at end of file diff --git a/Tests/AuthTests/__Snapshots__/RequestsTests/testVerifyOTPUsingEmail.1.txt b/Tests/AuthTests/__Snapshots__/RequestsTests/testVerifyOTPUsingEmail.1.txt index 885935d6..09aa49f4 100644 --- a/Tests/AuthTests/__Snapshots__/RequestsTests/testVerifyOTPUsingEmail.1.txt +++ b/Tests/AuthTests/__Snapshots__/RequestsTests/testVerifyOTPUsingEmail.1.txt @@ -1,8 +1,8 @@ curl \ --request POST \ - --header "Apikey: dummy.api.key" \ --header "Content-Type: application/json" \ --header "X-Client-Info: gotrue-swift/x.y.z" \ --header "X-Supabase-Api-Version: 2024-01-01" \ + --header "apiKey: dummy.api.key" \ --data "{\"email\":\"example@mail.com\",\"gotrue_meta_security\":{\"captcha_token\":\"captcha-token\"},\"token\":\"123456\",\"type\":\"magiclink\"}" \ "http://localhost:54321/auth/v1/verify?redirect_to=https://supabase.com" \ No newline at end of file diff --git a/Tests/AuthTests/__Snapshots__/RequestsTests/testVerifyOTPUsingPhone.1.txt b/Tests/AuthTests/__Snapshots__/RequestsTests/testVerifyOTPUsingPhone.1.txt index 38304842..afaa5d9f 100644 --- a/Tests/AuthTests/__Snapshots__/RequestsTests/testVerifyOTPUsingPhone.1.txt +++ b/Tests/AuthTests/__Snapshots__/RequestsTests/testVerifyOTPUsingPhone.1.txt @@ -1,8 +1,8 @@ curl \ --request POST \ - --header "Apikey: dummy.api.key" \ --header "Content-Type: application/json" \ --header "X-Client-Info: gotrue-swift/x.y.z" \ --header "X-Supabase-Api-Version: 2024-01-01" \ + --header "apiKey: dummy.api.key" \ --data "{\"gotrue_meta_security\":{\"captcha_token\":\"captcha-token\"},\"phone\":\"+1 202-918-2132\",\"token\":\"123456\",\"type\":\"sms\"}" \ "http://localhost:54321/auth/v1/verify" \ No newline at end of file diff --git a/Tests/AuthTests/__Snapshots__/RequestsTests/testVerifyOTPUsingTokenHash.1.txt b/Tests/AuthTests/__Snapshots__/RequestsTests/testVerifyOTPUsingTokenHash.1.txt index 2488d0c2..892d2f81 100644 --- a/Tests/AuthTests/__Snapshots__/RequestsTests/testVerifyOTPUsingTokenHash.1.txt +++ b/Tests/AuthTests/__Snapshots__/RequestsTests/testVerifyOTPUsingTokenHash.1.txt @@ -1,8 +1,8 @@ curl \ --request POST \ - --header "Apikey: dummy.api.key" \ --header "Content-Type: application/json" \ --header "X-Client-Info: gotrue-swift/x.y.z" \ --header "X-Supabase-Api-Version: 2024-01-01" \ + --header "apiKey: dummy.api.key" \ --data "{\"token_hash\":\"abc-def\",\"type\":\"email\"}" \ "http://localhost:54321/auth/v1/verify" \ No newline at end of file diff --git a/Tests/FunctionsTests/FunctionInvokeOptionsTests.swift b/Tests/FunctionsTests/FunctionInvokeOptionsTests.swift index 4a781007..ed9ffba5 100644 --- a/Tests/FunctionsTests/FunctionInvokeOptionsTests.swift +++ b/Tests/FunctionsTests/FunctionInvokeOptionsTests.swift @@ -28,8 +28,8 @@ final class FunctionInvokeOptionsTests: XCTestCase { let boundary = "Boundary-\(UUID().uuidString)" let contentType = "multipart/form-data; boundary=\(boundary)" let options = FunctionInvokeOptions( - headers: ["Content-Type": contentType], - body: "binary value".data(using: .utf8)! + headers: [.contentType: contentType], + body: Data("binary value".utf8) ) XCTAssertEqual(options.headers[.contentType], contentType) XCTAssertNotNil(options.body) diff --git a/Tests/FunctionsTests/FunctionsClientTests.swift b/Tests/FunctionsTests/FunctionsClientTests.swift index e4972dce..38b29745 100644 --- a/Tests/FunctionsTests/FunctionsClientTests.swift +++ b/Tests/FunctionsTests/FunctionsClientTests.swift @@ -1,10 +1,11 @@ import ConcurrencyExtras -@testable import Functions -import Helpers import HTTPTypes +import Helpers import TestHelpers import XCTest +@testable import Functions + #if canImport(FoundationNetworking) import FoundationNetworking #endif @@ -13,32 +14,32 @@ final class FunctionsClientTests: XCTestCase { let url = URL(string: "http://localhost:5432/functions/v1")! let apiKey = "supabase.anon.key" - lazy var sut = FunctionsClient(url: url, headers: ["Apikey": apiKey]) + lazy var sut = FunctionsClient(url: url, headers: [.apiKey: apiKey]) func testInit() async { let client = FunctionsClient( url: url, - headers: ["Apikey": apiKey], + headers: [.apiKey: apiKey], region: .saEast1 ) XCTAssertEqual(client.region, "sa-east-1") - XCTAssertEqual(client.headers[.init("Apikey")!], apiKey) - XCTAssertNotNil(client.headers[.init("X-Client-Info")!]) + XCTAssertEqual(client.headers[.apiKey], apiKey) + XCTAssertNotNil(client.headers[.xClientInfo]) } func testInvoke() async throws { let url = URL(string: "http://localhost:5432/functions/v1/hello_world")! let http = await HTTPClientMock() - .when { - $0.url.pathComponents.contains("hello_world") - } return: { _ in - try .stub(body: Empty()) + .when { request, bodyData in + return request.url!.pathComponents.contains("hello_world") + } return: { _, _ in + try TestStub.stub(body: Empty()) } let sut = FunctionsClient( url: self.url, - headers: ["Apikey": apiKey], + headers: [.apiKey: apiKey], region: nil, http: http ) @@ -47,24 +48,24 @@ final class FunctionsClientTests: XCTestCase { try await sut.invoke( "hello_world", - options: .init(headers: ["X-Custom-Key": "value"], body: body) + options: .init(headers: [.init("X-Custom-Key")!: "value"], body: body) ) let request = await http.receivedRequests.last - XCTAssertEqual(request?.url, url) - XCTAssertEqual(request?.method, .post) - XCTAssertEqual(request?.headers[.init("Apikey")!], apiKey) - XCTAssertEqual(request?.headers[.init("X-Custom-Key")!], "value") - XCTAssertEqual(request?.headers[.init("X-Client-Info")!], "functions-swift/\(Functions.version)") + XCTAssertEqual(request?.0.url, url) + XCTAssertEqual(request?.0.method, .post) + XCTAssertEqual(request?.0.headerFields[.apiKey], apiKey) + XCTAssertEqual(request?.0.headerFields[.init("X-Custom-Key")!], "value") + XCTAssertEqual(request?.0.headerFields[.xClientInfo], "functions-swift/\(Functions.version)") } func testInvokeWithCustomMethod() async throws { - let http = await HTTPClientMock().any { _ in try .stub(body: Empty()) } + let http = await HTTPClientMock().any { _, _ in try TestStub.stub(body: Empty()) } let sut = FunctionsClient( url: url, - headers: ["Apikey": apiKey], + headers: [.apiKey: apiKey], region: nil, http: http ) @@ -72,15 +73,15 @@ final class FunctionsClientTests: XCTestCase { try await sut.invoke("hello-world", options: .init(method: .delete)) let request = await http.receivedRequests.last - XCTAssertEqual(request?.method, .delete) + XCTAssertEqual(request?.0.method, .delete) } func testInvokeWithQuery() async throws { - let http = await HTTPClientMock().any { _ in try .stub(body: Empty()) } + let http = await HTTPClientMock().any { _, _ in try TestStub.stub(body: Empty()) } let sut = FunctionsClient( url: url, - headers: ["Apikey": apiKey], + headers: [.apiKey: apiKey], region: nil, http: http ) @@ -93,12 +94,12 @@ final class FunctionsClientTests: XCTestCase { ) let request = await http.receivedRequests.last - XCTAssertEqual(request?.urlRequest.url?.query, "key=value") + XCTAssertEqual(request?.0.url?.query, "key=value") } func testInvokeWithRegionDefinedInClient() async throws { let http = await HTTPClientMock() - .any { _ in try .stub(body: Empty()) } + .any { _, _ in try TestStub.stub(body: Empty()) } let sut = FunctionsClient( url: url, @@ -110,12 +111,12 @@ final class FunctionsClientTests: XCTestCase { try await sut.invoke("hello-world") let request = await http.receivedRequests.last - XCTAssertEqual(request?.headers[.xRegion], "ca-central-1") + XCTAssertEqual(request?.0.headerFields[.xRegion], "ca-central-1") } func testInvokeWithRegion() async throws { let http = await HTTPClientMock() - .any { _ in try .stub(body: Empty()) } + .any { _, _ in try TestStub.stub(body: Empty()) } let sut = FunctionsClient( url: url, @@ -127,12 +128,12 @@ final class FunctionsClientTests: XCTestCase { try await sut.invoke("hello-world", options: .init(region: .caCentral1)) let request = await http.receivedRequests.last - XCTAssertEqual(request?.headers[.xRegion], "ca-central-1") + XCTAssertEqual(request?.0.headerFields[.xRegion], "ca-central-1") } func testInvokeWithoutRegion() async throws { let http = await HTTPClientMock() - .any { _ in try .stub(body: Empty()) } + .any { _, _ in try TestStub.stub(body: Empty()) } let sut = FunctionsClient( url: url, @@ -144,16 +145,16 @@ final class FunctionsClientTests: XCTestCase { try await sut.invoke("hello-world") let request = await http.receivedRequests.last - XCTAssertNil(request?.headers[.xRegion]) + XCTAssertNil(request?.0.headerFields[.xRegion]) } func testInvoke_shouldThrow_URLError_badServerResponse() async { let sut = await FunctionsClient( url: url, - headers: ["Apikey": apiKey], + headers: [.apiKey: apiKey], region: nil, http: HTTPClientMock() - .any { _ in throw URLError(.badServerResponse) } + .any { _, _ in throw URLError(.badServerResponse) } ) do { @@ -168,10 +169,10 @@ final class FunctionsClientTests: XCTestCase { func testInvoke_shouldThrow_FunctionsError_httpError() async { let sut = await FunctionsClient( url: url, - headers: ["Apikey": apiKey], + headers: [.apiKey: apiKey], region: nil, http: HTTPClientMock() - .any { _ in try .stub(body: Empty(), statusCode: 300) } + .any { _, _ in try TestStub.stub(body: Empty(), statusCode: 300) } ) do { try await sut.invoke("hello_world") @@ -186,10 +187,10 @@ final class FunctionsClientTests: XCTestCase { func testInvoke_shouldThrow_FunctionsError_relayError() async { let sut = await FunctionsClient( url: url, - headers: ["Apikey": apiKey], + headers: [.apiKey: apiKey], region: nil, - http: HTTPClientMock().any { _ in - try .stub( + http: HTTPClientMock().any { _, _ in + try TestStub.stub( body: Empty(), headers: [.xRelayError: "true"] ) @@ -211,23 +212,18 @@ final class FunctionsClientTests: XCTestCase { } } -extension Helpers.HTTPResponse { +struct TestStub { static func stub( body: any Encodable, statusCode: Int = 200, headers: HTTPFields = .init() - ) throws -> Helpers.HTTPResponse { + ) throws -> (Data, HTTPResponse) { let data = try JSONEncoder().encode(body) - let response = HTTPURLResponse( - url: URL(string: "http://127.0.0.1")!, - statusCode: statusCode, - httpVersion: nil, - headerFields: headers.dictionary - )! - return HTTPResponse( - data: data, - response: response + let response = HTTPResponse( + status: .init(code: statusCode), + headerFields: headers ) + return (data, response) } } diff --git a/Tests/FunctionsTests/RequestTests.swift b/Tests/FunctionsTests/RequestTests.swift index 30d8bab1..e786db47 100644 --- a/Tests/FunctionsTests/RequestTests.swift +++ b/Tests/FunctionsTests/RequestTests.swift @@ -5,10 +5,15 @@ // Created by Guilherme Souza on 23/04/24. // -@testable import Functions import SnapshotTesting import XCTest +@testable import Functions + +#if canImport(FoundationNetworking) + import FoundationNetworking +#endif + final class RequestTests: XCTestCase { let url = URL(string: "http://localhost:5432/functions/v1")! let apiKey = "supabase.anon.key" @@ -33,7 +38,10 @@ final class RequestTests: XCTestCase { func testInvokeWithCustomHeader() async { await snapshot { - try await $0.invoke("hello-world", options: .init(headers: ["x-custom-key": "custom value"])) + try await $0.invoke( + "hello-world", + options: .init(headers: [.init("x-custom-key")!: "custom value"]) + ) } } @@ -52,10 +60,19 @@ final class RequestTests: XCTestCase { ) async { let sut = FunctionsClient( url: url, - headers: ["apikey": apiKey, "x-client-info": "functions-swift/x.y.z"] - ) { request in + headers: [.apiKey: apiKey, .xClientInfo: "functions-swift/x.y.z"] + ) { request, bodyData in await MainActor.run { - assertSnapshot(of: request, as: .curl, record: record, file: file, testName: testName, line: line) + var request = URLRequest(httpRequest: request)! + request.httpBody = bodyData + assertSnapshot( + of: request, + as: .curl, + record: record, + file: file, + testName: testName, + line: line + ) } throw NSError(domain: "Error", code: 0, userInfo: nil) } diff --git a/Tests/FunctionsTests/__Snapshots__/RequestTests/testInvokeWithBody.1.txt b/Tests/FunctionsTests/__Snapshots__/RequestTests/testInvokeWithBody.1.txt index a8f7bbe3..e33899f3 100644 --- a/Tests/FunctionsTests/__Snapshots__/RequestTests/testInvokeWithBody.1.txt +++ b/Tests/FunctionsTests/__Snapshots__/RequestTests/testInvokeWithBody.1.txt @@ -1,7 +1,7 @@ curl \ --request POST \ --header "Content-Type: application/json" \ - --header "apikey: supabase.anon.key" \ - --header "x-client-info: functions-swift/x.y.z" \ + --header "X-Client-Info: functions-swift/x.y.z" \ + --header "apiKey: supabase.anon.key" \ --data "{\"name\":\"Supabase\"}" \ "http://localhost:5432/functions/v1/hello-world" \ No newline at end of file diff --git a/Tests/FunctionsTests/__Snapshots__/RequestTests/testInvokeWithCustomHeader.1.txt b/Tests/FunctionsTests/__Snapshots__/RequestTests/testInvokeWithCustomHeader.1.txt index 3efebb9b..f0cafce8 100644 --- a/Tests/FunctionsTests/__Snapshots__/RequestTests/testInvokeWithCustomHeader.1.txt +++ b/Tests/FunctionsTests/__Snapshots__/RequestTests/testInvokeWithCustomHeader.1.txt @@ -1,6 +1,6 @@ curl \ --request POST \ - --header "apikey: supabase.anon.key" \ - --header "x-client-info: functions-swift/x.y.z" \ + --header "X-Client-Info: functions-swift/x.y.z" \ + --header "apiKey: supabase.anon.key" \ --header "x-custom-key: custom value" \ "http://localhost:5432/functions/v1/hello-world" \ No newline at end of file diff --git a/Tests/FunctionsTests/__Snapshots__/RequestTests/testInvokeWithCustomMethod.1.txt b/Tests/FunctionsTests/__Snapshots__/RequestTests/testInvokeWithCustomMethod.1.txt index a4460b69..178c9291 100644 --- a/Tests/FunctionsTests/__Snapshots__/RequestTests/testInvokeWithCustomMethod.1.txt +++ b/Tests/FunctionsTests/__Snapshots__/RequestTests/testInvokeWithCustomMethod.1.txt @@ -1,5 +1,5 @@ curl \ --request PATCH \ - --header "apikey: supabase.anon.key" \ - --header "x-client-info: functions-swift/x.y.z" \ + --header "X-Client-Info: functions-swift/x.y.z" \ + --header "apiKey: supabase.anon.key" \ "http://localhost:5432/functions/v1/hello-world" \ No newline at end of file diff --git a/Tests/FunctionsTests/__Snapshots__/RequestTests/testInvokeWithCustomRegion.1.txt b/Tests/FunctionsTests/__Snapshots__/RequestTests/testInvokeWithCustomRegion.1.txt index b7ebf5c7..65edd1ba 100644 --- a/Tests/FunctionsTests/__Snapshots__/RequestTests/testInvokeWithCustomRegion.1.txt +++ b/Tests/FunctionsTests/__Snapshots__/RequestTests/testInvokeWithCustomRegion.1.txt @@ -1,6 +1,6 @@ curl \ --request POST \ - --header "apikey: supabase.anon.key" \ - --header "x-client-info: functions-swift/x.y.z" \ + --header "X-Client-Info: functions-swift/x.y.z" \ + --header "apiKey: supabase.anon.key" \ --header "x-region: ap-northeast-1" \ "http://localhost:5432/functions/v1/hello-world" \ No newline at end of file diff --git a/Tests/FunctionsTests/__Snapshots__/RequestTests/testInvokeWithDefaultOptions.1.txt b/Tests/FunctionsTests/__Snapshots__/RequestTests/testInvokeWithDefaultOptions.1.txt index 053472bb..9e1d0834 100644 --- a/Tests/FunctionsTests/__Snapshots__/RequestTests/testInvokeWithDefaultOptions.1.txt +++ b/Tests/FunctionsTests/__Snapshots__/RequestTests/testInvokeWithDefaultOptions.1.txt @@ -1,5 +1,5 @@ curl \ --request POST \ - --header "apikey: supabase.anon.key" \ - --header "x-client-info: functions-swift/x.y.z" \ + --header "X-Client-Info: functions-swift/x.y.z" \ + --header "apiKey: supabase.anon.key" \ "http://localhost:5432/functions/v1/hello-world" \ No newline at end of file diff --git a/Tests/HelpersTests/ObservationTokenTests.swift b/Tests/HelpersTests/ObservationTokenTests.swift index 16eec655..6deb021e 100644 --- a/Tests/HelpersTests/ObservationTokenTests.swift +++ b/Tests/HelpersTests/ObservationTokenTests.swift @@ -7,9 +7,10 @@ import ConcurrencyExtras import Foundation -@testable import Helpers import XCTest +@testable import Helpers + final class ObservationTokenTests: XCTestCase { func testRemove() { let handle = ObservationToken() diff --git a/Tests/IntegrationTests/AuthClientIntegrationTests.swift b/Tests/IntegrationTests/AuthClientIntegrationTests.swift index d72637c8..fd64b382 100644 --- a/Tests/IntegrationTests/AuthClientIntegrationTests.swift +++ b/Tests/IntegrationTests/AuthClientIntegrationTests.swift @@ -5,12 +5,13 @@ // Created by Guilherme Souza on 27/03/24. // -@testable import Auth import ConcurrencyExtras import CustomDump import TestHelpers import XCTest +@testable import Auth + #if canImport(FoundationNetworking) import FoundationNetworking #endif @@ -24,8 +25,8 @@ final class AuthClientIntegrationTests: XCTestCase { configuration: AuthClient.Configuration( url: URL(string: "\(DotEnv.SUPABASE_URL)/auth/v1")!, headers: [ - "apikey": key, - "Authorization": "Bearer \(key)", + .apiKey: key, + .authorization: "Bearer \(key)", ], localStorage: InMemoryLocalStorage(), logger: nil diff --git a/Tests/IntegrationTests/PostgrestIntegrationTests.swift b/Tests/IntegrationTests/PostgrestIntegrationTests.swift index 6336fcfc..a8398ebf 100644 --- a/Tests/IntegrationTests/PostgrestIntegrationTests.swift +++ b/Tests/IntegrationTests/PostgrestIntegrationTests.swift @@ -38,7 +38,7 @@ final class IntegrationTests: XCTestCase { let client = PostgrestClient( url: URL(string: "\(DotEnv.SUPABASE_URL)/rest/v1")!, headers: [ - "Apikey": DotEnv.SUPABASE_ANON_KEY, + .apiKey: DotEnv.SUPABASE_ANON_KEY ], logger: nil ) diff --git a/Tests/IntegrationTests/Potsgrest/PostgresTransformsTests.swift b/Tests/IntegrationTests/Potsgrest/PostgresTransformsTests.swift index a0c5925e..99278637 100644 --- a/Tests/IntegrationTests/Potsgrest/PostgresTransformsTests.swift +++ b/Tests/IntegrationTests/Potsgrest/PostgresTransformsTests.swift @@ -14,7 +14,7 @@ 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 ) diff --git a/Tests/IntegrationTests/Potsgrest/PostgrestBasicTests.swift b/Tests/IntegrationTests/Potsgrest/PostgrestBasicTests.swift index 9912b4d1..ee86afb9 100644 --- a/Tests/IntegrationTests/Potsgrest/PostgrestBasicTests.swift +++ b/Tests/IntegrationTests/Potsgrest/PostgrestBasicTests.swift @@ -14,7 +14,7 @@ final class PostgrestBasicTests: 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 ) diff --git a/Tests/IntegrationTests/Potsgrest/PostgrestFilterTests.swift b/Tests/IntegrationTests/Potsgrest/PostgrestFilterTests.swift index 9ec7e3b1..b6fa13be 100644 --- a/Tests/IntegrationTests/Potsgrest/PostgrestFilterTests.swift +++ b/Tests/IntegrationTests/Potsgrest/PostgrestFilterTests.swift @@ -14,7 +14,7 @@ final class PostgrestFilterTests: 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 ) diff --git a/Tests/IntegrationTests/Potsgrest/PostgrestResourceEmbeddingTests.swift b/Tests/IntegrationTests/Potsgrest/PostgrestResourceEmbeddingTests.swift index c4acfbda..3f9d7af1 100644 --- a/Tests/IntegrationTests/Potsgrest/PostgrestResourceEmbeddingTests.swift +++ b/Tests/IntegrationTests/Potsgrest/PostgrestResourceEmbeddingTests.swift @@ -14,7 +14,7 @@ final class PostgrestResourceEmbeddingTests: 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 ) diff --git a/Tests/IntegrationTests/RealtimeIntegrationTests.swift b/Tests/IntegrationTests/RealtimeIntegrationTests.swift index 4b2b543a..000640ca 100644 --- a/Tests/IntegrationTests/RealtimeIntegrationTests.swift +++ b/Tests/IntegrationTests/RealtimeIntegrationTests.swift @@ -8,23 +8,24 @@ import ConcurrencyExtras import CustomDump import PostgREST -@testable import Realtime import Supabase import TestHelpers import XCTest +@testable import Realtime + final class RealtimeIntegrationTests: XCTestCase { let realtime = RealtimeClientV2( url: URL(string: "\(DotEnv.SUPABASE_URL)/realtime/v1")!, options: RealtimeClientOptions( - headers: ["apikey": DotEnv.SUPABASE_ANON_KEY] + headers: [.apiKey: DotEnv.SUPABASE_ANON_KEY] ) ) let db = PostgrestClient( url: URL(string: "\(DotEnv.SUPABASE_URL)/rest/v1")!, headers: [ - "apikey": DotEnv.SUPABASE_ANON_KEY, + .apiKey: DotEnv.SUPABASE_ANON_KEY ] ) @@ -73,14 +74,14 @@ final class RealtimeIntegrationTests: XCTestCase { [ "event": "test", "payload": [ - "value": 1, + "value": 1 ], "type": "broadcast", ], [ "event": "test", "payload": [ - "value": 2, + "value": 2 ], "type": "broadcast", ], @@ -151,7 +152,7 @@ final class RealtimeIntegrationTests: XCTestCase { expectNoDifference( joins, [ - [], // This is the first PRESENCE_STATE event. + [], // This is the first PRESENCE_STATE event. [UserState(email: "test@supabase.com")], [UserState(email: "test2@supabase.com")], [], @@ -161,7 +162,7 @@ final class RealtimeIntegrationTests: XCTestCase { expectNoDifference( leaves, [ - [], // This is the first PRESENCE_STATE event. + [], // This is the first PRESENCE_STATE event. [], [UserState(email: "test@supabase.com")], [UserState(email: "test2@supabase.com")], diff --git a/Tests/IntegrationTests/StorageClientIntegrationTests.swift b/Tests/IntegrationTests/StorageClientIntegrationTests.swift index 2d073eab..df32ed7f 100644 --- a/Tests/IntegrationTests/StorageClientIntegrationTests.swift +++ b/Tests/IntegrationTests/StorageClientIntegrationTests.swift @@ -14,7 +14,7 @@ final class StorageClientIntegrationTests: XCTestCase { configuration: StorageClientConfiguration( url: URL(string: "\(DotEnv.SUPABASE_URL)/storage/v1")!, headers: [ - "Authorization": "Bearer \(DotEnv.SUPABASE_SERVICE_ROLE_KEY)", + .authorization: "Bearer \(DotEnv.SUPABASE_SERVICE_ROLE_KEY)" ], logger: nil ) @@ -36,7 +36,10 @@ final class StorageClientIntegrationTests: XCTestCase { buckets = try await storage.listBuckets() XCTAssertTrue(buckets.contains { $0.id == bucket.id }) - try await storage.updateBucket(bucketName, options: BucketOptions(allowedMimeTypes: ["image/jpeg"])) + try await storage.updateBucket( + bucketName, + options: BucketOptions(allowedMimeTypes: ["image/jpeg"]) + ) bucket = try await storage.getBucket(bucketName) XCTAssertEqual(bucket.allowedMimeTypes, ["image/jpeg"]) diff --git a/Tests/IntegrationTests/StorageFileIntegrationTests.swift b/Tests/IntegrationTests/StorageFileIntegrationTests.swift index 188090a9..02f5a912 100644 --- a/Tests/IntegrationTests/StorageFileIntegrationTests.swift +++ b/Tests/IntegrationTests/StorageFileIntegrationTests.swift @@ -5,10 +5,11 @@ // Created by Guilherme Souza on 07/05/24. // +import HTTPTypes +import Helpers import InlineSnapshotTesting import Storage import XCTest -import Helpers #if canImport(FoundationNetworking) import FoundationNetworking @@ -19,7 +20,7 @@ final class StorageFileIntegrationTests: XCTestCase { configuration: StorageClientConfiguration( url: URL(string: "\(DotEnv.SUPABASE_URL)/storage/v1")!, headers: [ - "Authorization": "Bearer \(DotEnv.SUPABASE_SERVICE_ROLE_KEY)" + .authorization: "Bearer \(DotEnv.SUPABASE_SERVICE_ROLE_KEY)" ], logger: nil ) @@ -378,9 +379,13 @@ final class StorageFileIntegrationTests: XCTestCase { let publicURL = try storage.from(bucketName).getPublicURL(path: uploadPath) - let (_, response) = try await URLSession.shared.data(from: publicURL) - let httpResponse = try XCTUnwrap(response as? HTTPURLResponse) - let cacheControl = try XCTUnwrap(httpResponse.value(forHTTPHeaderField: "cache-control")) + let request = HTTPRequest( + method: .get, + url: publicURL + ) + + let (_, response) = try await URLSession.shared.data(for: request) + let cacheControl = try XCTUnwrap(response.headerFields[.cacheControl]) XCTAssertEqual(cacheControl, "public, max-age=14400") } diff --git a/Tests/PostgRESTTests/BuildURLRequestTests.swift b/Tests/PostgRESTTests/BuildURLRequestTests.swift index c4ba68d1..f909f4b5 100644 --- a/Tests/PostgRESTTests/BuildURLRequestTests.swift +++ b/Tests/PostgRESTTests/BuildURLRequestTests.swift @@ -1,5 +1,6 @@ import ConcurrencyExtras import Foundation +import HTTPTypes import Helpers import SnapshotTesting import XCTest @@ -49,15 +50,18 @@ final class BuildURLRequestTests: XCTestCase { let client = PostgrestClient( url: url, schema: nil, - headers: ["X-Client-Info": "postgrest-swift/x.y.z"], + headers: [.xClientInfo: "postgrest-swift/x.y.z"], logger: nil, - fetch: { request in + fetch: { request, bodyData in guard let runningTestCase = await runningTestCase.value else { XCTFail("execute called without a runningTestCase set.") - return (Data(), URLResponse.empty()) + return (Data(), HTTPResponse(status: .ok)) } await MainActor.run { [runningTestCase] in + var request = URLRequest(httpRequest: request)! + request.httpBody = bodyData + assertSnapshot( of: request, as: .curl, @@ -69,7 +73,7 @@ final class BuildURLRequestTests: XCTestCase { ) } - return (Data(), URLResponse.empty()) + return (Data(), HTTPResponse(status: .ok)) }, encoder: encoder ) @@ -251,26 +255,7 @@ final class BuildURLRequestTests: XCTestCase { func testSessionConfiguration() { let client = PostgrestClient(url: url, schema: nil, logger: nil) - let clientInfoHeader = client.configuration.headers["X-Client-Info"] + let clientInfoHeader = client.configuration.headers[.xClientInfo] XCTAssertNotNil(clientInfoHeader) } } - -extension URLResponse { - // Windows and Linux don't have the ability to empty initialize a URLResponse like `URLResponse()` - // so - // We provide a function that can give us the right value on an platform. - // See https://github.com/apple/swift-corelibs-foundation/pull/4778 - fileprivate static func empty() -> URLResponse { - #if os(Windows) || os(Linux) - URLResponse( - url: .init(string: "https://supabase.com")!, - mimeType: nil, - expectedContentLength: 0, - textEncodingName: nil - ) - #else - URLResponse() - #endif - } -} diff --git a/Tests/PostgRESTTests/JSONTests.swift b/Tests/PostgRESTTests/JSONTests.swift index dabf2d97..5b339933 100644 --- a/Tests/PostgRESTTests/JSONTests.swift +++ b/Tests/PostgRESTTests/JSONTests.swift @@ -5,16 +5,17 @@ // Created by Guilherme Souza on 01/07/24. // -@testable import PostgREST import XCTest +@testable import PostgREST + final class JSONTests: XCTestCase { func testDecodeJSON() throws { let json = """ - { - "created_at": "2024-06-15T18:12:04+00:00" - } - """.data(using: .utf8)! + { + "created_at": "2024-06-15T18:12:04+00:00" + } + """.data(using: .utf8)! struct Value: Decodable { var createdAt: Date diff --git a/Tests/PostgRESTTests/PostgrestBuilderTests.swift b/Tests/PostgRESTTests/PostgrestBuilderTests.swift index 1fa049a4..41466c28 100644 --- a/Tests/PostgRESTTests/PostgrestBuilderTests.swift +++ b/Tests/PostgRESTTests/PostgrestBuilderTests.swift @@ -5,19 +5,23 @@ // Created by Guilherme Souza on 20/08/24. // -@testable import PostgREST import XCTest +@testable import PostgREST + final class PostgrestBuilderTests: XCTestCase { let url = URL(string: "http://localhost:54321/rest/v1")! func testCustomHeaderOnAPerCallBasis() throws { - let postgrest1 = PostgrestClient(url: url, headers: ["apikey": "foo"], logger: nil) - let postgrest2 = try postgrest1.rpc("void_func").setHeader(name: .init("apikey")!, value: "bar") + let postgrest1 = PostgrestClient(url: url, headers: [.apiKey: "foo"], logger: nil) + let postgrest2 = try postgrest1.rpc("void_func").setHeader(name: .apiKey, value: "bar") // Original client object isn't affected - XCTAssertEqual(postgrest1.from("users").select().mutableState.request.headers[.init("apikey")!], "foo") + XCTAssertEqual( + postgrest1.from("users").select().mutableState.request.headerFields[.apiKey], + "foo" + ) // Derived client object uses new header value - XCTAssertEqual(postgrest2.mutableState.request.headers[.init("apikey")!], "bar") + XCTAssertEqual(postgrest2.mutableState.request.headerFields[.apiKey], "bar") } } diff --git a/Tests/PostgRESTTests/PostgrestResponseTests.swift b/Tests/PostgRESTTests/PostgrestResponseTests.swift index 09637818..9c65311e 100644 --- a/Tests/PostgRESTTests/PostgrestResponseTests.swift +++ b/Tests/PostgRESTTests/PostgrestResponseTests.swift @@ -1,3 +1,4 @@ +import HTTPTypes import XCTest @testable import PostgREST @@ -10,12 +11,10 @@ class PostgrestResponseTests: XCTestCase { func testInit() { // Prepare data and response let data = Data() - let response = HTTPURLResponse( - url: URL(string: "http://example.com")!, - statusCode: 200, - httpVersion: nil, - headerFields: ["Content-Range": "bytes 0-100/200"] - )! + let response = HTTPResponse( + status: .init(code: 200), + headerFields: [.contentRange: "bytes 0-100/200"] + ) let value = "Test Value" // Create the PostgrestResponse instance @@ -32,12 +31,10 @@ class PostgrestResponseTests: XCTestCase { func testInitWithNoCount() { // Prepare data and response let data = Data() - let response = HTTPURLResponse( - url: URL(string: "http://example.com")!, - statusCode: 200, - httpVersion: nil, - headerFields: ["Content-Range": "*"] - )! + let response = HTTPResponse( + status: .init(code: 200), + headerFields: [.contentRange: "*"] + ) let value = "Test Value" // Create the PostgrestResponse instance diff --git a/Tests/RealtimeTests/MockWebSocketClient.swift b/Tests/RealtimeTests/MockWebSocketClient.swift index bcabc958..5580b122 100644 --- a/Tests/RealtimeTests/MockWebSocketClient.swift +++ b/Tests/RealtimeTests/MockWebSocketClient.swift @@ -7,9 +7,10 @@ import ConcurrencyExtras import Foundation -@testable import Realtime import XCTestDynamicOverlay +@testable import Realtime + #if canImport(FoundationNetworking) import FoundationNetworking #endif diff --git a/Tests/RealtimeTests/PostgresJoinConfigTests.swift b/Tests/RealtimeTests/PostgresJoinConfigTests.swift index bb695d18..64db6c56 100644 --- a/Tests/RealtimeTests/PostgresJoinConfigTests.swift +++ b/Tests/RealtimeTests/PostgresJoinConfigTests.swift @@ -5,9 +5,10 @@ // Created by Guilherme Souza on 26/12/23. // -@testable import Realtime import XCTest +@testable import Realtime + final class PostgresJoinConfigTests: XCTestCase { func testSameConfigButDifferentIdAreEqual() { let config1 = PostgresJoinConfig( diff --git a/Tests/RealtimeTests/RealtimeTests.swift b/Tests/RealtimeTests/RealtimeTests.swift index 35a318cc..b7956208 100644 --- a/Tests/RealtimeTests/RealtimeTests.swift +++ b/Tests/RealtimeTests/RealtimeTests.swift @@ -1,5 +1,6 @@ import ConcurrencyExtras import CustomDump +import HTTPTypes import Helpers import InlineSnapshotTesting import TestHelpers @@ -33,7 +34,7 @@ final class RealtimeTests: XCTestCase { sut = RealtimeClientV2( url: url, options: RealtimeClientOptions( - headers: ["apikey": apiKey], + headers: [.apiKey: apiKey], heartbeatInterval: 1, reconnectDelay: 1, timeoutInterval: 2 @@ -298,17 +299,12 @@ final class RealtimeTests: XCTestCase { } func testBroadcastWithHTTP() async throws { - await http.when { - $0.url.path.hasSuffix("broadcast") - } return: { _ in - HTTPResponse( - data: "{}".data(using: .utf8)!, - response: HTTPURLResponse( - url: self.sut.broadcastURL, - statusCode: 200, - httpVersion: nil, - headerFields: nil - )! + await http.when { request, bodyData in + request.url!.path.hasSuffix("broadcast") + } return: { _, _ in + ( + Data("{}".utf8), + HTTPResponse(status: .init(code: 200)) ) } @@ -319,7 +315,10 @@ final class RealtimeTests: XCTestCase { try await channel.broadcast(event: "test", message: ["value": 42]) let request = await http.receivedRequests.last - assertInlineSnapshot(of: request?.urlRequest, as: .raw(pretty: true)) { + var urlRequest = request.map { URLRequest(httpRequest: $0.0) } + urlRequest??.httpBody = request?.1 + + assertInlineSnapshot(of: urlRequest as? URLRequest, as: .raw(pretty: true)) { """ POST https://localhost:54321/realtime/v1/api/broadcast Authorization: Bearer anon.api.key diff --git a/Tests/RealtimeTests/_PushTests.swift b/Tests/RealtimeTests/_PushTests.swift index 67efc7a1..871c0bed 100644 --- a/Tests/RealtimeTests/_PushTests.swift +++ b/Tests/RealtimeTests/_PushTests.swift @@ -6,10 +6,11 @@ // import ConcurrencyExtras -@testable import Realtime import TestHelpers import XCTest +@testable import Realtime + final class _PushTests: XCTestCase { var ws: MockWebSocketClient! var socket: RealtimeClientV2! @@ -27,7 +28,7 @@ final class _PushTests: XCTestCase { socket = RealtimeClientV2( url: URL(string: "https://localhost:54321/v1/realtime")!, options: RealtimeClientOptions( - headers: ["apiKey": "apikey"] + headers: [.apiKey: "apikey"] ), ws: ws, http: HTTPClientMock() diff --git a/Tests/StorageTests/SupabaseStorageClient+Test.swift b/Tests/StorageTests/SupabaseStorageClient+Test.swift index ac10137f..bb9dace9 100644 --- a/Tests/StorageTests/SupabaseStorageClient+Test.swift +++ b/Tests/StorageTests/SupabaseStorageClient+Test.swift @@ -18,9 +18,9 @@ extension SupabaseStorageClient { configuration: StorageClientConfiguration( url: URL(string: supabaseURL)!, headers: [ - "Authorization": "Bearer \(apiKey)", - "Apikey": apiKey, - "X-Client-Info": "storage-swift/x.y.z", + .authorization: "Bearer \(apiKey)", + .apiKey: apiKey, + .xClientInfo: "storage-swift/x.y.z", ], session: session, logger: nil diff --git a/Tests/StorageTests/SupabaseStorageTests.swift b/Tests/StorageTests/SupabaseStorageTests.swift index 5742538d..e05dd777 100644 --- a/Tests/StorageTests/SupabaseStorageTests.swift +++ b/Tests/StorageTests/SupabaseStorageTests.swift @@ -1,5 +1,6 @@ import CustomDump import Foundation +import HTTPTypes import InlineSnapshotTesting import XCTest import XCTestDynamicOverlay @@ -15,8 +16,7 @@ final class SupabaseStorageTests: XCTestCase { let bucketId = "tests" var sessionMock = StorageHTTPSession( - fetch: unimplemented("StorageHTTPSession.fetch"), - upload: unimplemented("StorageHTTPSession.upload") + fetch: unimplemented("StorageHTTPSession.fetch") ) func testGetPublicURL() throws { @@ -58,24 +58,20 @@ final class SupabaseStorageTests: XCTestCase { } func testCreateSignedURLs() async throws { - sessionMock.fetch = { _ in + sessionMock.fetch = { _, _ in ( - """ - [ - { - "signedURL": "/sign/file1.txt?token=abc.def.ghi" - }, - { - "signedURL": "/sign/file2.txt?token=abc.def.ghi" - }, - ] - """.data(using: .utf8)!, - HTTPURLResponse( - url: self.supabaseURL, - statusCode: 200, - httpVersion: nil, - headerFields: nil - )! + Data( + """ + [ + { + "signedURL": "/sign/file1.txt?token=abc.def.ghi" + }, + { + "signedURL": "/sign/file2.txt?token=abc.def.ghi" + }, + ] + """.utf8), + HTTPResponse(status: .init(code: 200)) ) } @@ -96,16 +92,19 @@ final class SupabaseStorageTests: XCTestCase { func testUploadData() async throws { testingBoundary.setValue("alamofire.boundary.c21f947c1c7b0c57") - sessionMock.fetch = { request in + sessionMock.fetch = { request, bodyData in + var request = URLRequest(httpRequest: request)! + request.httpBody = bodyData + assertInlineSnapshot(of: request, as: .curl) { #""" curl \ --request POST \ - --header "Apikey: test.api.key" \ --header "Authorization: Bearer test.api.key" \ --header "Cache-Control: max-age=14400" \ --header "Content-Type: multipart/form-data; boundary=alamofire.boundary.c21f947c1c7b0c57" \ --header "X-Client-Info: storage-swift/x.y.z" \ + --header "apiKey: test.api.key" \ --header "x-upsert: false" \ --data "--alamofire.boundary.c21f947c1c7b0c57\#r Content-Disposition: form-data; name=\"cacheControl\"\#r @@ -126,18 +125,16 @@ final class SupabaseStorageTests: XCTestCase { """# } return ( - """ - { - "Id": "tests/file1.txt", - "Key": "tests/file1.txt" - } - """.data(using: .utf8)!, - HTTPURLResponse( - url: self.supabaseURL, - statusCode: 200, - httpVersion: nil, - headerFields: nil - )! + Data( + """ + { + "Id": "tests/file1.txt", + "Key": "tests/file1.txt" + } + """.utf8), + HTTPResponse( + status: .init(code: 200) + ) ) } @@ -157,33 +154,31 @@ final class SupabaseStorageTests: XCTestCase { func testUploadFileURL() async throws { testingBoundary.setValue("alamofire.boundary.c21f947c1c7b0c57") - sessionMock.fetch = { request in + sessionMock.fetch = { request, bodyData in + var request = URLRequest(httpRequest: request)! + request.httpBody = bodyData assertInlineSnapshot(of: request, as: .curl) { #""" curl \ --request POST \ - --header "Apikey: test.api.key" \ --header "Authorization: Bearer test.api.key" \ --header "Cache-Control: max-age=3600" \ --header "Content-Type: multipart/form-data; boundary=alamofire.boundary.c21f947c1c7b0c57" \ --header "X-Client-Info: storage-swift/x.y.z" \ + --header "apiKey: test.api.key" \ --header "x-upsert: false" \ "http://localhost:54321/storage/v1/object/tests/sadcat.jpg" """# } return ( - """ - { - "Id": "tests/file1.txt", - "Key": "tests/file1.txt" - } - """.data(using: .utf8)!, - HTTPURLResponse( - url: self.supabaseURL, - statusCode: 200, - httpVersion: nil, - headerFields: nil - )! + Data( + """ + { + "Id": "tests/file1.txt", + "Key": "tests/file1.txt" + } + """.utf8), + HTTPResponse(status: .init(code: 200)) ) } diff --git a/Tests/SupabaseTests/SupabaseClientTests.swift b/Tests/SupabaseTests/SupabaseClientTests.swift index c487177d..122ccdbf 100644 --- a/Tests/SupabaseTests/SupabaseClientTests.swift +++ b/Tests/SupabaseTests/SupabaseClientTests.swift @@ -1,10 +1,12 @@ -@testable import Auth import CustomDump -@testable import Functions +import HTTPTypes import IssueReporting +import XCTest + +@testable import Auth +@testable import Functions @testable import Realtime @testable import Supabase -import XCTest final class AuthLocalStorageMock: AuthLocalStorage { func store(key _: String, value _: Data) throws {} @@ -27,7 +29,7 @@ final class SupabaseClientTests: XCTestCase { let logger = Logger() let customSchema = "custom_schema" let localStorage = AuthLocalStorageMock() - let customHeaders = ["header_field": "header_value"] + let customHeaders: HTTPFields = [.init("header_field")!: "header_value"] let client = SupabaseClient( supabaseURL: URL(string: "https://project-ref.supabase.co")!, @@ -47,7 +49,7 @@ final class SupabaseClientTests: XCTestCase { region: .apNortheast1 ), realtime: RealtimeClientOptions( - headers: ["custom_realtime_header_key": "custom_realtime_header_value"] + headers: [.init("custom_realtime_header_key")!: "custom_realtime_header_value"] ) ) ) @@ -62,15 +64,14 @@ final class SupabaseClientTests: XCTestCase { ) XCTAssertEqual( - client.headers, + client.headers, [ - "X-Client-Info": "supabase-swift/\(Supabase.version)", - "Apikey": "ANON_KEY", - "header_field": "header_value", - "Authorization": "Bearer ANON_KEY", + .xClientInfo: "supabase-swift/\(Supabase.version)", + .apiKey: "ANON_KEY", + .init("header_field")!: "header_value", + .authorization: "Bearer ANON_KEY", ] ) - expectNoDifference(client._headers.dictionary, client.headers) XCTAssertEqual(client.functions.region, "ap-northeast-1") @@ -78,9 +79,10 @@ final class SupabaseClientTests: XCTestCase { XCTAssertEqual(realtimeURL.absoluteString, "https://project-ref.supabase.co/realtime/v1") let realtimeOptions = client.realtimeV2.options - let expectedRealtimeHeader = client._headers.merging(with: [ - .init("custom_realtime_header_key")!: "custom_realtime_header_value"] - ) + let expectedRealtimeHeader = client.headers.merging([ + .init("custom_realtime_header_key")!: "custom_realtime_header_value" + ]) { $1 } + expectNoDifference(realtimeOptions.headers, expectedRealtimeHeader) XCTAssertIdentical(realtimeOptions.logger as? Logger, logger) From 4eedb75d33e2d6e5b17d40c0b81c335e6efade83 Mon Sep 17 00:00:00 2001 From: "zunda.dev@gmail.com" Date: Sat, 11 Jan 2025 21:11:16 +0900 Subject: [PATCH 3/3] fix urlrequest --- Tests/RealtimeTests/RealtimeTests.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Tests/RealtimeTests/RealtimeTests.swift b/Tests/RealtimeTests/RealtimeTests.swift index 3612b6b8..05828b08 100644 --- a/Tests/RealtimeTests/RealtimeTests.swift +++ b/Tests/RealtimeTests/RealtimeTests.swift @@ -343,8 +343,9 @@ final class RealtimeTests: XCTestCase { try await channel.broadcast(event: "test", message: ["value": 42]) - let request = await http.receivedRequests.last?.0 - let urlReqest = request.map { URLRequest(httpRequest: $0)! } + let request = await http.receivedRequests.last + var urlReqest = request.map { URLRequest(httpRequest: $0.0)! }! + urlReqest.httpBody = request?.1 assertInlineSnapshot(of: urlReqest, as: .raw(pretty: true)) { """ POST https://localhost:54321/realtime/v1/api/broadcast