GrpcClient.swift 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. // Copyright 2024 Google LLC
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. import Foundation
  15. import FirebaseAppCheck
  16. import FirebaseAuth
  17. import FirebaseCoreInternal
  18. import FirebaseSharedSwift
  19. import GRPC
  20. import NIOCore
  21. import NIOHPACK
  22. import NIOPosix
  23. import OSLog
  24. import SwiftProtobuf
  25. @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
  26. actor GrpcClient: CustomStringConvertible {
  27. nonisolated let description: String
  28. private let projectId: String
  29. private let threadPoolSize = 1
  30. private let serverSettings: DataConnectSettings
  31. private let connectorConfig: ConnectorConfig
  32. private let connectorName: String // Fully qualified connector name
  33. private let auth: Auth
  34. private let appCheck: AppCheck?
  35. private enum RequestHeaders {
  36. static let googRequestParamsHeader = "x-goog-request-params"
  37. static let authorizationHeader = "x-firebase-auth-token"
  38. static let appCheckHeader = "X-Firebase-AppCheck"
  39. }
  40. private let googRequestHeaderValue: String
  41. private lazy var client: Google_Firebase_Dataconnect_V1alpha_ConnectorServiceAsyncClient? = {
  42. do {
  43. FirebaseLogger.dataConnect.debug("\(self.description) initialization starts.")
  44. let group = PlatformSupport.makeEventLoopGroup(loopCount: threadPoolSize)
  45. let channel = try GRPCChannelPool.with(
  46. target: .host(serverSettings.host, port: serverSettings.port),
  47. transportSecurity: serverSettings
  48. .sslEnabled ? .tls(GRPCTLSConfiguration.makeClientDefault(compatibleWith: group)) :
  49. .plaintext,
  50. eventLoopGroup: group
  51. )
  52. FirebaseLogger.dataConnect.debug("\(self.description) has been created.")
  53. return Google_Firebase_Dataconnect_V1alpha_ConnectorServiceAsyncClient(channel: channel)
  54. } catch {
  55. FirebaseLogger.dataConnect.error("Error:\(error) when creating \(self.description).")
  56. return nil
  57. }
  58. }()
  59. init(projectId: String, settings: DataConnectSettings, connectorConfig: ConnectorConfig,
  60. auth: Auth,
  61. appCheck: AppCheck?) {
  62. self.projectId = projectId
  63. serverSettings = settings
  64. self.connectorConfig = connectorConfig
  65. self.auth = auth
  66. self.appCheck = appCheck
  67. connectorName =
  68. "projects/\(projectId)/locations/\(connectorConfig.location)/services/\(connectorConfig.serviceId)/connectors/\(connectorConfig.connector)"
  69. googRequestHeaderValue = "location=\(self.connectorConfig.location)&frontend=data"
  70. description = """
  71. GrpcClient: \
  72. projectId=\(projectId) \
  73. connector=\(connectorConfig.connector) \
  74. host=\(serverSettings.host) \
  75. port=\(serverSettings.port) \
  76. ssl=\(serverSettings.sslEnabled)
  77. """
  78. }
  79. func executeQuery<ResultType: Decodable,
  80. VariableType: OperationVariable>(request: QueryRequest<VariableType>,
  81. resultType: ResultType
  82. .Type)
  83. async throws -> OperationResult<ResultType> {
  84. guard let client else {
  85. FirebaseLogger.dataConnect
  86. .error("When calling executeQuery(), grpc client has not been configured.")
  87. throw DataConnectError.grpcNotConfigured
  88. }
  89. let codec = Codec()
  90. let grpcRequest = try codec.createQueryRequestProto(
  91. connectorName: connectorName,
  92. request: request
  93. )
  94. do {
  95. try FirebaseLogger.dataConnect
  96. .debug("executeQuery() sends grpc request: \(grpcRequest.jsonString()).")
  97. let results = try await client.executeQuery(grpcRequest, callOptions: createCallOptions())
  98. try FirebaseLogger.dataConnect
  99. .debug("executeQuery() receives response: \(results.jsonString()).")
  100. // Not doing error decoding here
  101. if let decodedResults = try codec.decode(result: results.data, asType: resultType) {
  102. return OperationResult(data: decodedResults)
  103. } else {
  104. // In future, set this as error in OperationResult
  105. try FirebaseLogger.dataConnect
  106. .error("executeQuery() response: \(results.jsonString()) decode failed.")
  107. throw DataConnectError.decodeFailed
  108. }
  109. } catch {
  110. try FirebaseLogger.dataConnect
  111. .error(
  112. "executeQuery() with request: \(grpcRequest.jsonString()) grpc call FAILED with \(error)."
  113. )
  114. throw error
  115. }
  116. }
  117. func executeMutation<ResultType: Decodable,
  118. VariableType: OperationVariable>(request: MutationRequest<VariableType>,
  119. resultType: ResultType
  120. .Type)
  121. async throws -> OperationResult<ResultType> {
  122. guard let client else {
  123. FirebaseLogger.dataConnect
  124. .error("When calling executeMutation(), grpc client has not been configured.")
  125. throw DataConnectError.grpcNotConfigured
  126. }
  127. let codec = Codec()
  128. let grpcRequest = try codec.createMutationRequestProto(
  129. connectorName: connectorName,
  130. request: request
  131. )
  132. do {
  133. try FirebaseLogger.dataConnect
  134. .debug("executeMutation() sends grpc request: \(grpcRequest.jsonString()).")
  135. let results = try await client.executeMutation(grpcRequest, callOptions: createCallOptions())
  136. try FirebaseLogger.dataConnect
  137. .debug("executeMutation() receives response: \(results.jsonString()).")
  138. if let decodedResults = try codec.decode(result: results.data, asType: resultType) {
  139. return OperationResult(data: decodedResults)
  140. } else {
  141. try FirebaseLogger.dataConnect
  142. .error("executeMutation() response: \(results.jsonString()) decode failed.")
  143. throw DataConnectError.decodeFailed
  144. }
  145. } catch {
  146. try FirebaseLogger.dataConnect
  147. .error(
  148. "executeMutation() with request: \(grpcRequest.jsonString()) grpc call FAILED with \(error)."
  149. )
  150. throw error
  151. }
  152. }
  153. private func createCallOptions() async -> CallOptions {
  154. var headers = HPACKHeaders()
  155. headers.add(name: RequestHeaders.googRequestParamsHeader, value: googRequestHeaderValue)
  156. // Add Auth token if available
  157. do {
  158. if let token = try await auth.currentUser?.getIDToken() {
  159. headers.add(name: RequestHeaders.authorizationHeader, value: "\(token)")
  160. print("added Auth token \(token)")
  161. } else {
  162. FirebaseLogger.dataConnect.debug("No auth token available. Not adding auth header.")
  163. }
  164. } catch {
  165. FirebaseLogger.dataConnect
  166. .debug("Cannot get auth token successfully due to: \(error). Not adding auth header.")
  167. }
  168. // Add AppCheck token if available
  169. do {
  170. if let token = try await appCheck?.token(forcingRefresh: false) {
  171. headers.add(name: RequestHeaders.appCheckHeader, value: token.token)
  172. FirebaseLogger.dataConnect
  173. .debug("App Check token added: \(token.token)")
  174. } else {
  175. FirebaseLogger.dataConnect
  176. .debug("App Check token unavailable. Not adding App Check header.")
  177. }
  178. } catch {
  179. FirebaseLogger.dataConnect
  180. .debug(
  181. "Cannot get App Check token successfully due to: \(error). Not adding App Check header."
  182. )
  183. }
  184. let options = CallOptions(customMetadata: headers)
  185. return options
  186. }
  187. }