Line data Source code
1 : part of apptive_grid_network; 2 : 3 : /// Api Client to communicate with the ApptiveGrid Backend 4 : class ApptiveGridClient { 5 : /// Creates an ApiClient 6 2 : ApptiveGridClient({ 7 : this.options = const ApptiveGridOptions(), 8 2 : }) : _client = http.Client(), 9 2 : _authenticator = ApptiveGridAuthenticator(options: options); 10 : 11 : /// Creates an Api Client on the Basis of a [http.Client] 12 : /// 13 : /// this should only be used for testing in order to pass in a Mocked [http.Client] 14 1 : @visibleForTesting 15 : ApptiveGridClient.fromClient( 16 : http.Client httpClient, { 17 : this.options = const ApptiveGridOptions(), 18 : ApptiveGridAuthenticator? authenticator, 19 : }) : _client = httpClient, 20 : _authenticator = authenticator ?? 21 1 : ApptiveGridAuthenticator(options: options, httpClient: httpClient); 22 : 23 : /// Configuraptions 24 : ApptiveGridOptions options; 25 : 26 : final ApptiveGridAuthenticator _authenticator; 27 : 28 : final http.Client _client; 29 : 30 : /// Close the connection on the httpClient 31 2 : void dispose() { 32 4 : _client.close(); 33 4 : _authenticator.dispose(); 34 : } 35 : 36 : /// Headers that are used for multiple Calls 37 1 : @visibleForTesting 38 1 : Map<String, String> get headers => (<String, String?>{ 39 2 : HttpHeaders.authorizationHeader: _authenticator.header, 40 : HttpHeaders.contentTypeHeader: ContentType.json, 41 2 : }..removeWhere((key, value) => value == null)) 42 3 : .map((key, value) => MapEntry(key, value!)); 43 : 44 : /// Loads a [FormData] represented by [formUri] 45 : /// 46 : /// Based on [formUri] this might require Authentication 47 : /// throws [Response] if the request fails 48 1 : Future<FormData> loadForm({ 49 : required FormUri formUri, 50 : }) async { 51 1 : if (formUri.needsAuthorization) { 52 3 : await _authenticator.checkAuthentication(); 53 : } 54 6 : final url = Uri.parse('${options.environment.url}${formUri.uriString}'); 55 4 : final response = await _client.get(url, headers: headers); 56 2 : if (response.statusCode >= 400) { 57 : throw response; 58 : } 59 3 : return FormData.fromJson(json.decode(response.body)); 60 : } 61 : 62 : /// Performs a [FormAction] using [formData] 63 : /// 64 : /// if this returns a [http.Response] with a [http.Response.statusCode] >= 400 it means that the Item was saved in [options.cache] 65 : /// throws [Response] if the request fails 66 1 : Future<http.Response> performAction( 67 : FormAction action, 68 : FormData formData, { 69 : bool saveToPendingItems = true, 70 : }) async { 71 1 : final actionItem = ActionItem(action: action, data: formData); 72 6 : final uri = Uri.parse('${options.environment.url}${action.uri}'); 73 2 : final request = http.Request(action.method, uri); 74 3 : request.body = jsonEncode(formData.toRequestObject()); 75 : 76 : // ignore: prefer_function_declarations_over_variables 77 1 : final handleError = (error) async { 78 : // TODO: Filter out Errors that happened because the Input was not correct 79 : // in that case don't save the Action and throw the error 80 2 : if (saveToPendingItems && options.cache != null) { 81 4 : await options.cache!.addPendingActionItem(actionItem); 82 1 : if (error is http.Response) { 83 : return error; 84 : } else { 85 2 : return http.Response(error.toString(), 400); 86 : } 87 : } 88 : throw error; 89 : }; 90 : late http.Response response; 91 3 : request.headers.addAll(headers); 92 : try { 93 3 : final streamResponse = await _client.send(request); 94 2 : response = await http.Response.fromStream(streamResponse); 95 : } catch (e) { 96 : // Catch all Exception for compatibility Reasons between Web and non Web Apps 97 : return handleError(e); 98 : } 99 : 100 2 : if (response.statusCode >= 400) { 101 0 : return handleError(response); 102 : } 103 : // Action was performed successfully. Remove it from pending Actions 104 4 : await options.cache?.removePendingActionItem(actionItem); 105 0 : return response; 106 : } 107 : 108 : /// Loads a [Grid] represented by [gridUri] 109 : /// 110 : /// Requires Authorization 111 : /// throws [Response] if the request fails 112 1 : Future<Grid> loadGrid({ 113 : required GridUri gridUri, 114 : }) async { 115 3 : await _authenticator.checkAuthentication(); 116 6 : final url = Uri.parse('${options.environment.url}${gridUri.uriString}'); 117 4 : final response = await _client.get(url, headers: headers); 118 2 : if (response.statusCode >= 400) { 119 : throw response; 120 : } 121 3 : return Grid.fromJson(json.decode(response.body)); 122 : } 123 : 124 : /// Get the [User] that is authenticated 125 : /// 126 : /// Requires Authorization 127 : /// throws [Response] if the request fails 128 1 : Future<User> getMe() async { 129 3 : await _authenticator.checkAuthentication(); 130 : 131 5 : final url = Uri.parse('${options.environment.url}/api/users/me'); 132 4 : final response = await _client.get(url, headers: headers); 133 2 : if (response.statusCode >= 400) { 134 : throw response; 135 : } 136 3 : return User.fromJson(json.decode(response.body)); 137 : } 138 : 139 : /// Get the [Space] represented by [spaceUri] 140 : /// 141 : /// Requires Authorization 142 : /// throws [Response] if the request fails 143 1 : Future<Space> getSpace({ 144 : required SpaceUri spaceUri, 145 : }) async { 146 3 : await _authenticator.checkAuthentication(); 147 : 148 6 : final url = Uri.parse('${options.environment.url}${spaceUri.uriString}'); 149 4 : final response = await _client.get(url, headers: headers); 150 2 : if (response.statusCode >= 400) { 151 : throw response; 152 : } 153 3 : return Space.fromJson(json.decode(response.body)); 154 : } 155 : 156 : /// Get all [FormUri]s that are contained in a [Grid] represented by [gridUri] 157 : /// 158 : /// Requires Authorization 159 : /// throws [Response] if the request fails 160 1 : Future<List<FormUri>> getForms({ 161 : required GridUri gridUri, 162 : }) async { 163 3 : await _authenticator.checkAuthentication(); 164 : 165 : final url = 166 6 : Uri.parse('${options.environment.url}${gridUri.uriString}/forms'); 167 4 : final response = await _client.get(url, headers: headers); 168 2 : if (response.statusCode >= 400) { 169 : throw response; 170 : } 171 2 : return (json.decode(response.body) as List) 172 3 : .map((e) => FormUri.fromUri(e)) 173 1 : .toList(); 174 : } 175 : 176 : /// Get all [GridViewUri]s that are contained in a [Grid] represented by [gridUri] 177 : /// 178 : /// Requires Authorization 179 : /// throws [Response] if the request fails 180 1 : Future<List<GridViewUri>> getGridViews({ 181 : required GridUri gridUri, 182 : }) async { 183 3 : await _authenticator.checkAuthentication(); 184 : 185 : final url = 186 6 : Uri.parse('${options.environment.url}${gridUri.uriString}/views'); 187 4 : final response = await _client.get(url, headers: headers); 188 2 : if (response.statusCode >= 400) { 189 : throw response; 190 : } 191 2 : return (json.decode(response.body) as List) 192 3 : .map((e) => GridViewUri.fromUri(e)) 193 1 : .toList(); 194 : } 195 : 196 : /// Creates and returns a [FormUri] filled with the Data represented by [entityUri] 197 : /// 198 : /// Requires Authorization 199 : /// throws [Response] if the request fails 200 1 : Future<FormUri> getEditLink({ 201 : required EntityUri entityUri, 202 : required String formId, 203 : }) async { 204 3 : await _authenticator.checkAuthentication(); 205 : 206 : final url = 207 6 : Uri.parse('${options.environment.url}${entityUri.uriString}/EditLink'); 208 : 209 3 : final response = await _client.post( 210 : url, 211 1 : headers: headers, 212 2 : body: jsonEncode({ 213 : 'formId': formId, 214 : }), 215 : ); 216 : 217 2 : if (response.statusCode >= 400) { 218 : throw response; 219 : } 220 : 221 4 : return FormUri.fromUri((json.decode(response.body) as Map)['uri']); 222 : } 223 : 224 : /// Authenticate the User 225 : /// 226 : /// This will open a Webpage for the User Auth 227 1 : Future<Credential?> authenticate() { 228 2 : return _authenticator.authenticate(); 229 : } 230 : 231 : /// Logs out the user 232 1 : Future<void> logout() { 233 2 : return _authenticator.logout(); 234 : } 235 : 236 : /// Checks if the User is currently authenticated 237 3 : bool get isAuthenticated => _authenticator.isAuthenticated; 238 : 239 : /// Updates the Environment for the client and handle necessary changes in the Authenticator 240 1 : Future<void> updateEnvironment(ApptiveGridEnvironment environment) async { 241 3 : final currentRealm = options.environment.authRealm; 242 : 243 2 : if (currentRealm != environment.authRealm) { 244 3 : await _authenticator.logout(); 245 : } 246 : 247 3 : options = options.copyWith(environment: environment); 248 3 : _authenticator.options = options; 249 : } 250 : 251 : /// Tries to send pending [ActionItem]s that are stored in [options.cache] 252 2 : Future sendPendingActions() async { 253 8 : final pendingActions = await options.cache?.getPendingActionItems() ?? []; 254 : 255 3 : for (final action in pendingActions) { 256 : try { 257 2 : await performAction( 258 1 : action.action, 259 1 : action.data, 260 : saveToPendingItems: false, // don't resubmit this to pending items 261 : ); 262 1 : } on http.Response catch (_) { 263 : // Was not able to submit this action 264 : } 265 : } 266 : } 267 : }