GrpcClient.swift 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  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 appCheckEnabled = false
  35. private let appCheck: AppCheck?
  36. private enum RequestHeaders {
  37. static let googRequestParamsHeader = "x-goog-request-params"
  38. static let authorizationHeader = "x-firebase-auth-token"
  39. static let appCheckHeader = "X-Firebase-AppCheck"
  40. }
  41. private let googRequestHeaderValue: String
  42. private lazy var client: Google_Firebase_Dataconnect_V1alpha_ConnectorServiceAsyncClient? = {
  43. do {
  44. FirebaseLogger.dataConnect.debug("\(self.description) initialization starts.")
  45. let group = PlatformSupport.makeEventLoopGroup(loopCount: threadPoolSize)
  46. let channel = try GRPCChannelPool.with(
  47. target: .host(serverSettings.host, port: serverSettings.port),
  48. transportSecurity: serverSettings
  49. .sslEnabled ? .tls(GRPCTLSConfiguration.makeClientDefault(compatibleWith: group)) :
  50. .plaintext,
  51. eventLoopGroup: group
  52. )
  53. FirebaseLogger.dataConnect.debug("\(self.description) has been created.")
  54. return Google_Firebase_Dataconnect_V1alpha_ConnectorServiceAsyncClient(channel: channel)
  55. } catch {
  56. FirebaseLogger.dataConnect.error("Error:\(error) when creating \(self.description).")
  57. return nil
  58. }
  59. }()
  60. init(projectId: String, settings: DataConnectSettings, connectorConfig: ConnectorConfig,
  61. auth: Auth,
  62. appCheck: AppCheck?) {
  63. self.projectId = projectId
  64. serverSettings = settings
  65. self.connectorConfig = connectorConfig
  66. self.auth = auth
  67. self.appCheck = appCheck
  68. connectorName =
  69. "projects/\(projectId)/locations/\(connectorConfig.location)/services/\(connectorConfig.serviceId)/connectors/\(connectorConfig.connector)"
  70. googRequestHeaderValue = "location=\(self.connectorConfig.location)&frontend=data"
  71. description = """
  72. GrpcClient: \
  73. projectId=\(projectId) \
  74. connector=\(connectorConfig.connector) \
  75. host=\(serverSettings.host) \
  76. port=\(serverSettings.port) \
  77. ssl=\(serverSettings.sslEnabled)
  78. """
  79. }
  80. func executeQuery<ResultType: Decodable,
  81. VariableType: OperationVariable>(request: QueryRequest<VariableType>,
  82. resultType: ResultType
  83. .Type)
  84. async throws -> OperationResult<ResultType> {
  85. guard let client else {
  86. FirebaseLogger.dataConnect
  87. .error("When calling executeQuery(), grpc client has not been configured.")
  88. throw DataConnectError.grpcNotConfigured
  89. }
  90. let codec = Codec()
  91. let grpcRequest = try codec.createQueryRequestProto(
  92. connectorName: connectorName,
  93. request: request
  94. )
  95. do {
  96. try FirebaseLogger.dataConnect
  97. .debug("executeQuery() sends grpc request: \(grpcRequest.jsonString()).")
  98. let results = try await client.executeQuery(grpcRequest, callOptions: createCallOptions())
  99. try FirebaseLogger.dataConnect
  100. .debug("executeQuery() receives response: \(results.jsonString()).")
  101. // Not doing error decoding here
  102. if let decodedResults = try codec.decode(result: results.data, asType: resultType) {
  103. return OperationResult(data: decodedResults)
  104. } else {
  105. // In future, set this as error in OperationResult
  106. try FirebaseLogger.dataConnect
  107. .error("executeQuery() response: \(results.jsonString()) decode failed.")
  108. throw DataConnectError.decodeFailed
  109. }
  110. } catch {
  111. try FirebaseLogger.dataConnect
  112. .error(
  113. "executeQuery() with request: \(grpcRequest.jsonString()) grpc call FAILED with \(error)."
  114. )
  115. throw error
  116. }
  117. }
  118. func executeMutation<ResultType: Decodable,
  119. VariableType: OperationVariable>(request: MutationRequest<VariableType>,
  120. resultType: ResultType
  121. .Type)
  122. async throws -> OperationResult<ResultType> {
  123. guard let client else {
  124. FirebaseLogger.dataConnect
  125. .error("When calling executeMutation(), grpc client has not been configured.")
  126. throw DataConnectError.grpcNotConfigured
  127. }
  128. let codec = Codec()
  129. let grpcRequest = try codec.createMutationRequestProto(
  130. connectorName: connectorName,
  131. request: request
  132. )
  133. do {
  134. try FirebaseLogger.dataConnect
  135. .debug("executeMutation() sends grpc request: \(grpcRequest.jsonString()).")
  136. let results = try await client.executeMutation(grpcRequest, callOptions: createCallOptions())
  137. try FirebaseLogger.dataConnect
  138. .debug("executeMutation() receives response: \(results.jsonString()).")
  139. if let decodedResults = try codec.decode(result: results.data, asType: resultType) {
  140. return OperationResult(data: decodedResults)
  141. } else {
  142. try FirebaseLogger.dataConnect
  143. .error("executeMutation() response: \(results.jsonString()) decode failed.")
  144. throw DataConnectError.decodeFailed
  145. }
  146. } catch {
  147. try FirebaseLogger.dataConnect
  148. .error(
  149. "executeMutation() with request: \(grpcRequest.jsonString()) grpc call FAILED with \(error)."
  150. )
  151. throw error
  152. }
  153. }
  154. private func createCallOptions() async -> CallOptions {
  155. var headers = HPACKHeaders()
  156. headers.add(name: RequestHeaders.googRequestParamsHeader, value: googRequestHeaderValue)
  157. // Add Auth token if available
  158. do {
  159. if let token = try await auth.currentUser?.getIDToken() {
  160. headers.add(name: RequestHeaders.authorizationHeader, value: "\(token)")
  161. print("added Auth token \(token)")
  162. } else {
  163. FirebaseLogger.dataConnect.debug("No auth token available. Not adding auth header.")
  164. }
  165. } catch {
  166. FirebaseLogger.dataConnect
  167. .debug("Cannot get auth token successfully due to: \(error). Not adding auth header.")
  168. }
  169. // Add AppCheck token if available
  170. if appCheckEnabled {
  171. do {
  172. if let token = try await appCheck?.token(forcingRefresh: false) {
  173. headers.add(name: RequestHeaders.appCheckHeader, value: "\(token)")
  174. FirebaseLogger.dataConnect
  175. .debug("App Check token added: \(token)")
  176. } else {
  177. FirebaseLogger.dataConnect
  178. .debug("App Check token unavailable. Not adding App Check header.")
  179. }
  180. } catch {
  181. FirebaseLogger.dataConnect
  182. .debug(
  183. "Cannot get App Check token successfully due to: \(error). Not adding App Check header."
  184. )
  185. }
  186. }
  187. let options = CallOptions(customMetadata: headers)
  188. return options
  189. }
  190. }