LCOV - code coverage report
Current view: top level - repository - remote_adapter.dart (source / functions) Hit Total Coverage
Test: lcov.info Lines: 228 235 97.0 %
Date: 2022-06-06 11:59:57 Functions: 0 0 -

          Line data    Source code
       1             : part of flutter_data;
       2             : 
       3             : /// An adapter base class for all remote operations for type [T].
       4             : ///
       5             : /// Includes:
       6             : ///
       7             : ///  - Remote methods such as [_RemoteAdapter.findAll] or [_RemoteAdapter.save]
       8             : ///  - Configuration methods and getters like [_RemoteAdapter.baseUrl] or [_RemoteAdapter.urlForFindAll]
       9             : ///  - Serialization methods like [_RemoteAdapterSerialization.serialize]
      10             : ///  - Watch methods such as [Repository.watchOneNotifier]
      11             : ///  - Access to the [_RemoteAdapter.graph] for subclasses or mixins
      12             : ///
      13             : /// This class is meant to be extended via mixing in new adapters.
      14             : /// This can be done with the [DataRepository] annotation on a [DataModel] class:
      15             : ///
      16             : /// ```
      17             : /// @JsonSerializable()
      18             : /// @DataRepository([MyAppAdapter])
      19             : /// class Todo with DataModel<Todo> {
      20             : ///   @override
      21             : ///   final int? id;
      22             : ///   final String title;
      23             : ///   final bool completed;
      24             : ///
      25             : ///   Todo({this.id, required this.title, this.completed = false});
      26             : /// }
      27             : /// ```
      28             : ///
      29             : /// Identity in this layer is enforced by IDs.
      30             : // ignore: library_private_types_in_public_api
      31             : class RemoteAdapter<T extends DataModel<T>> = _RemoteAdapter<T>
      32             :     with _RemoteAdapterSerialization<T>, _RemoteAdapterWatch<T>;
      33             : 
      34             : abstract class _RemoteAdapter<T extends DataModel<T>> with _Lifecycle {
      35          11 :   @protected
      36             :   _RemoteAdapter(this.localAdapter, [this._internalHolder]);
      37             : 
      38             :   @protected
      39             :   @visibleForTesting
      40             :   @nonVirtual
      41             :   final LocalAdapter<T> localAdapter;
      42             : 
      43             :   /// A [GraphNotifier] instance also available to adapters
      44          11 :   @protected
      45             :   @nonVirtual
      46          22 :   GraphNotifier get graph => localAdapter.graph;
      47             : 
      48             :   // None of these fields below can be late finals as they might be re-initialized
      49             :   Map<String, RemoteAdapter>? _adapters;
      50             :   bool? _remote;
      51             :   Reader? _read;
      52             : 
      53             :   /// All adapters for the relationship subgraph of [T] and their relationships.
      54             :   ///
      55             :   /// This [Map] is typically required when initializing new models, and passed as-is.
      56           4 :   @protected
      57             :   @nonVirtual
      58           4 :   Map<String, RemoteAdapter> get adapters => _adapters!;
      59             : 
      60             :   /// Give access to the dependency injection system
      61           7 :   @nonVirtual
      62           7 :   Reader get read => _read!;
      63             : 
      64             :   /// INTERNAL: DO NOT USE
      65          11 :   @visibleForTesting
      66             :   @protected
      67             :   @nonVirtual
      68          11 :   String get internalType => DataHelpers.getType<T>();
      69             : 
      70             :   /// The pluralized and downcased [DataHelpers.getType<T>] version of type [T]
      71             :   /// by default.
      72             :   ///
      73             :   /// Example: [T] as `Post` has a [type] of `posts`.
      74          12 :   String get type => internalType;
      75             : 
      76             :   /// ONLY FOR FLUTTER DATA INTERNAL USE
      77             :   Watcher? internalWatch;
      78             :   final InternalHolder<T>? _internalHolder;
      79             : 
      80             :   /// Set log level.
      81             :   // ignore: prefer_final_fields
      82             :   int _logLevel = 0;
      83             : 
      84             :   /// Returns the base URL for this type [T].
      85             :   ///
      86             :   /// Typically used in a generic adapter (i.e. one shared by all types)
      87             :   /// so it should be e.g. `http://jsonplaceholder.typicode.com/`
      88             :   ///
      89             :   /// For specific paths to this type [T], see [urlForFindAll], [urlForFindOne], etc
      90           6 :   @protected
      91             :   String get baseUrl => 'https://override-base-url-in-adapter/';
      92             : 
      93             :   /// Returns URL for [findAll]. Defaults to [type].
      94           4 :   @protected
      95           4 :   String urlForFindAll(Map<String, dynamic> params) => type;
      96             : 
      97             :   /// Returns HTTP method for [findAll]. Defaults to `GET`.
      98           4 :   @protected
      99             :   DataRequestMethod methodForFindAll(Map<String, dynamic> params) =>
     100             :       DataRequestMethod.GET;
     101             : 
     102             :   /// Returns URL for [findOne]. Defaults to [type]/[id].
     103           4 :   @protected
     104           8 :   String urlForFindOne(id, Map<String, dynamic> params) => '$type/$id';
     105             : 
     106             :   /// Returns HTTP method for [findOne]. Defaults to `GET`.
     107           4 :   @protected
     108             :   DataRequestMethod methodForFindOne(id, Map<String, dynamic> params) =>
     109             :       DataRequestMethod.GET;
     110             : 
     111             :   /// Returns URL for [save]. Defaults to [type]/[id] (if [id] is present).
     112           6 :   @protected
     113             :   String urlForSave(id, Map<String, dynamic> params) =>
     114          14 :       id != null ? '$type/$id' : type;
     115             : 
     116             :   /// Returns HTTP method for [save]. Defaults to `PATCH` if [id] is present,
     117             :   /// or `POST` otherwise.
     118           6 :   @protected
     119             :   DataRequestMethod methodForSave(id, Map<String, dynamic> params) =>
     120             :       id != null ? DataRequestMethod.PATCH : DataRequestMethod.POST;
     121             : 
     122             :   /// Returns URL for [delete]. Defaults to [type]/[id].
     123           3 :   @protected
     124           6 :   String urlForDelete(id, Map<String, dynamic> params) => '$type/$id';
     125             : 
     126             :   /// Returns HTTP method for [delete]. Defaults to `DELETE`.
     127           3 :   @protected
     128             :   DataRequestMethod methodForDelete(id, Map<String, dynamic> params) =>
     129             :       DataRequestMethod.DELETE;
     130             : 
     131             :   /// A [Map] representing default HTTP query parameters. Defaults to empty.
     132             :   ///
     133             :   /// It can return a [Future], so that adapters overriding this method
     134             :   /// have a chance to call async methods.
     135             :   ///
     136             :   /// Example:
     137             :   /// ```
     138             :   /// @override
     139             :   /// FutureOr<Map<String, dynamic>> get defaultParams async {
     140             :   ///   final token = await _localStorage.get('token');
     141             :   ///   return await super.defaultParams..addAll({'token': token});
     142             :   /// }
     143             :   /// ```
     144           9 :   @protected
     145           9 :   FutureOr<Map<String, dynamic>> get defaultParams => {};
     146             : 
     147             :   /// A [Map] representing default HTTP headers.
     148             :   ///
     149             :   /// Initial default is: `{'Content-Type': 'application/json'}`.
     150             :   ///
     151             :   /// It can return a [Future], so that adapters overriding this method
     152             :   /// have a chance to call async methods.
     153             :   ///
     154             :   /// Example:
     155             :   /// ```
     156             :   /// @override
     157             :   /// FutureOr<Map<String, String>> get defaultHeaders async {
     158             :   ///   final token = await _localStorage.get('token');
     159             :   ///   return await super.defaultHeaders..addAll({'Authorization': token});
     160             :   /// }
     161             :   /// ```
     162           9 :   @protected
     163             :   FutureOr<Map<String, String>> get defaultHeaders =>
     164           9 :       {'Content-Type': 'application/json'};
     165             : 
     166             :   // lifecycle methods
     167             : 
     168             :   @mustCallSuper
     169          11 :   Future<void> onInitialized() async {
     170             :     // wipe out orphans
     171          22 :     graph.removeOrphanNodes();
     172             :   }
     173             : 
     174             :   @mustCallSuper
     175             :   @nonVirtual
     176          11 :   Future<RemoteAdapter<T>> initialize(
     177             :       {bool? remote,
     178             :       required Map<String, RemoteAdapter> adapters,
     179             :       required Reader read}) async {
     180          11 :     if (isInitialized) return this as RemoteAdapter<T>;
     181             : 
     182             :     // initialize attributes
     183          11 :     _adapters = adapters;
     184          11 :     _remote = remote ?? true;
     185          11 :     _read = read;
     186             : 
     187          33 :     await localAdapter.initialize();
     188             : 
     189             :     // hook for clients
     190          22 :     await onInitialized();
     191             : 
     192             :     return this as RemoteAdapter<T>;
     193             :   }
     194             : 
     195          11 :   @override
     196          22 :   bool get isInitialized => localAdapter.isInitialized;
     197             : 
     198          10 :   @override
     199             :   void dispose() {
     200          20 :     localAdapter.dispose();
     201             :   }
     202             : 
     203             :   // serialization interface
     204             : 
     205             :   /// Returns a [DeserializedData] object when deserializing a given [data].
     206             :   @protected
     207             :   @visibleForTesting
     208             :   Future<DeserializedData<T>> deserialize(Object? data);
     209             : 
     210             :   /// Returns a serialized version of a model of [T],
     211             :   /// as a [Map<String, dynamic>] ready to be JSON-encoded.
     212             :   @protected
     213             :   @visibleForTesting
     214             :   Future<Map<String, dynamic>> serialize(T model,
     215             :       {bool withRelationships = true});
     216             : 
     217             :   // caching
     218             : 
     219             :   /// Returns whether calling [findAll] should trigger a remote call.
     220             :   ///
     221             :   /// Meant to be overriden. Defaults to [remote].
     222           4 :   @protected
     223             :   bool shouldLoadRemoteAll(
     224             :     bool remote,
     225             :     Map<String, dynamic> params,
     226             :     Map<String, String> headers,
     227             :   ) =>
     228             :       remote;
     229             : 
     230             :   /// Returns whether calling [findOne] should initiate an HTTP call.
     231             :   ///
     232             :   /// Meant to be overriden. Defaults to [remote].
     233           6 :   @protected
     234             :   bool shouldLoadRemoteOne(
     235             :     Object? id,
     236             :     bool remote,
     237             :     Map<String, dynamic> params,
     238             :     Map<String, String> headers,
     239             :   ) =>
     240             :       remote;
     241             : 
     242             :   // remote implementation
     243             : 
     244           4 :   Future<List<T>?> findAll({
     245             :     bool? remote,
     246             :     bool? background,
     247             :     Map<String, dynamic>? params,
     248             :     Map<String, String>? headers,
     249             :     bool? syncLocal,
     250             :     OnSuccessAll<T>? onSuccess,
     251             :     OnErrorAll<T>? onError,
     252             :     DataRequestLabel? label,
     253             :   }) async {
     254           1 :     remote ??= _remote;
     255             :     background ??= false;
     256             :     syncLocal ??= false;
     257          12 :     params = await defaultParams & params;
     258          12 :     headers = await defaultHeaders & headers;
     259             : 
     260           8 :     label = DataRequestLabel('findAll', type: internalType, withParent: label);
     261             : 
     262             :     late List<T>? models;
     263             : 
     264           4 :     if (!shouldLoadRemoteAll(remote!, params, headers) || background) {
     265          11 :       models = localAdapter.findAll()?.toImmutableList();
     266             :       if (models != null) {
     267           3 :         log(label,
     268           6 :             'returned ${models.toShortLog()} from local storage${background ? ' and loading in the background' : ''}');
     269             :       }
     270             :       if (!background) {
     271             :         return models;
     272             :       }
     273             :     }
     274             : 
     275           4 :     final future = sendRequest<List<T>>(
     276          20 :       baseUrl.asUri / urlForFindAll(params) & params,
     277           4 :       method: methodForFindAll(params),
     278             :       headers: headers,
     279             :       label: label,
     280           4 :       onSuccess: (data, label) async {
     281             :         if (syncLocal!) {
     282           6 :           await localAdapter.clear();
     283             :         }
     284           8 :         onSuccess ??= (data, label, _) => this.onSuccess<List<T>>(data, label);
     285           4 :         return onSuccess!.call(data, label, this as RemoteAdapter<T>);
     286             :       },
     287           3 :       onError: (e, label) async {
     288           2 :         onError ??= (e, label, _) => this.onError<List<T>>(e, label);
     289           3 :         return onError!.call(e, label, this as RemoteAdapter<T>);
     290             :       },
     291             :     );
     292             : 
     293             :     if (background && models != null) {
     294             :       // ignore: unawaited_futures
     295           0 :       future.then((_) => Future.value(_));
     296             :       return models;
     297             :     } else {
     298           7 :       return await future ?? <T>[];
     299             :     }
     300             :   }
     301             : 
     302           6 :   Future<T?> findOne(
     303             :     Object id, {
     304             :     bool? remote,
     305             :     bool? background,
     306             :     Map<String, dynamic>? params,
     307             :     Map<String, String>? headers,
     308             :     OnSuccessOne<T>? onSuccess,
     309             :     OnErrorOne<T>? onError,
     310             :     DataRequestLabel? label,
     311             :   }) async {
     312           3 :     remote ??= _remote;
     313             :     background ??= false;
     314          18 :     params = await defaultParams & params;
     315          18 :     headers = await defaultHeaders & headers;
     316             : 
     317           6 :     final resolvedId = _resolveId(id);
     318             :     late T? model;
     319             : 
     320           6 :     label = DataRequestLabel('findOne',
     321          12 :         type: internalType, id: resolvedId?.toString(), withParent: label);
     322             : 
     323           6 :     if (!shouldLoadRemoteOne(id, remote!, params, headers) || background) {
     324          18 :       final key = graph.getKeyForId(internalType, resolvedId,
     325           6 :           keyIfAbsent: id is T ? id._key : null);
     326          12 :       model = localAdapter.findOne(key);
     327             :       if (model != null) {
     328           6 :         log(label,
     329           6 :             'returned from local storage${background ? ' and loading in the background' : ''}');
     330             :       }
     331             :       if (!background) {
     332             :         return model;
     333             :       }
     334             :     }
     335             : 
     336           4 :     final future = sendRequest(
     337          20 :       baseUrl.asUri / urlForFindOne(id, params) & params,
     338           4 :       method: methodForFindOne(id, params),
     339             :       headers: headers,
     340             :       label: label,
     341           4 :       onSuccess: (data, label) {
     342           8 :         onSuccess ??= (data, label, _) => this.onSuccess<T>(data, label);
     343           4 :         return onSuccess!.call(data, label, this as RemoteAdapter<T>);
     344             :       },
     345           3 :       onError: (e, label) async {
     346           4 :         onError ??= (e, label, _) => this.onError<T>(e, label);
     347           3 :         return onError!.call(e, label, this as RemoteAdapter<T>);
     348             :       },
     349             :     );
     350             : 
     351             :     if (background && model != null) {
     352             :       // ignore: unawaited_futures
     353           0 :       future.then((_) => Future.value(_));
     354             :       return model;
     355             :     } else {
     356           4 :       return await future;
     357             :     }
     358             :   }
     359             : 
     360           9 :   Future<T> save(
     361             :     T model, {
     362             :     bool? remote,
     363             :     Map<String, dynamic>? params,
     364             :     Map<String, String>? headers,
     365             :     OnSuccessOne<T>? onSuccess,
     366             :     OnErrorOne<T>? onError,
     367             :     DataRequestLabel? label,
     368             :   }) async {
     369           9 :     remote ??= _remote;
     370             : 
     371          27 :     params = await defaultParams & params;
     372          27 :     headers = await defaultHeaders & headers;
     373             : 
     374             :     // ensure model is saved
     375          36 :     await localAdapter.save(model._key!, model);
     376             : 
     377           9 :     label = DataRequestLabel('save',
     378           9 :         type: internalType,
     379          18 :         id: model.id?.toString(),
     380             :         model: model,
     381             :         withParent: label);
     382             : 
     383           9 :     if (remote == false) {
     384           7 :       log(label, 'saved in local storage only');
     385             :       return model;
     386             :     }
     387             : 
     388          12 :     final serialized = await serialize(model);
     389           6 :     final body = json.encode(serialized);
     390             : 
     391          36 :     final uri = baseUrl.asUri / urlForSave(model.id, params) & params;
     392          12 :     final method = methodForSave(model.id, params);
     393             : 
     394          12 :     final result = await sendRequest<T>(
     395             :       uri,
     396             :       method: method,
     397             :       headers: headers,
     398             :       body: body,
     399             :       label: label,
     400           5 :       onSuccess: (data, label) {
     401          10 :         onSuccess ??= (data, label, _) => this.onSuccess<T>(data, label);
     402           5 :         return onSuccess!.call(data, label, this as RemoteAdapter<T>);
     403             :       },
     404           2 :       onError: (e, label) async {
     405           2 :         onError ??= (e, label, _) => this.onError<T>(e, label);
     406           2 :         return onError!.call(e, label, this as RemoteAdapter<T>);
     407             :       },
     408             :     );
     409             :     return result ?? model;
     410             :   }
     411             : 
     412          10 :   T saveLocal(T model, {bool notify = true}) {
     413          30 :     localAdapter.save(model._key!, model, notify: notify);
     414             :     return model;
     415             :   }
     416             : 
     417           5 :   Future<T?> delete(
     418             :     Object model, {
     419             :     bool? remote,
     420             :     Map<String, dynamic>? params,
     421             :     Map<String, String>? headers,
     422             :     OnSuccessOne<T>? onSuccess,
     423             :     OnErrorOne<T>? onError,
     424             :     DataRequestLabel? label,
     425             :   }) async {
     426           3 :     remote ??= _remote;
     427             : 
     428          15 :     params = await defaultParams & params;
     429          15 :     headers = await defaultHeaders & headers;
     430             : 
     431           5 :     final id = _resolveId(model);
     432           5 :     final key = keyForModelOrId(model);
     433             : 
     434           5 :     label = DataRequestLabel('delete',
     435          10 :         type: internalType, id: id.toString(), withParent: label);
     436             : 
     437             :     if (key != null) {
     438           5 :       if (remote == false) {
     439           3 :         log(label, 'deleted in local storage only');
     440             :       }
     441          15 :       await localAdapter.delete(key);
     442             :     }
     443             : 
     444           5 :     if (remote == true && id != null) {
     445           6 :       return await sendRequest(
     446          15 :         baseUrl.asUri / urlForDelete(id, params) & params,
     447           3 :         method: methodForDelete(id, params),
     448             :         headers: headers,
     449             :         label: label,
     450           3 :         onSuccess: (data, label) {
     451           6 :           onSuccess ??= (data, label, _) => this.onSuccess<T>(data, label);
     452           3 :           return onSuccess!.call(data, label, this as RemoteAdapter<T>);
     453             :         },
     454           1 :         onError: (e, label) async {
     455           0 :           onError ??= (e, label, _) => this.onError<T>(e, label);
     456           1 :           return onError!.call(e, label, this as RemoteAdapter<T>);
     457             :         },
     458             :       );
     459             :     }
     460             :     return null;
     461             :   }
     462             : 
     463           1 :   void deleteLocal(T model, {bool notify = true}) {
     464           3 :     localAdapter.delete(model._key!, notify: notify);
     465             :   }
     466             : 
     467           3 :   Future<void> clear() => localAdapter.clear();
     468             : 
     469             :   // http
     470             : 
     471             :   /// An [http.Client] used to make an HTTP request.
     472             :   ///
     473             :   /// This getter returns a new client every time
     474             :   /// as by default they are used once and then closed.
     475           0 :   @protected
     476             :   @visibleForTesting
     477           0 :   http.Client get httpClient => http.Client();
     478             : 
     479             :   /// The function used to perform an HTTP request and return an [R].
     480             :   ///
     481             :   /// **IMPORTANT**:
     482             :   ///  - [uri] takes the FULL `Uri` including query parameters
     483             :   ///  - [headers] does NOT include ANY defaults such as [defaultHeaders]
     484             :   ///  (unless you omit the argument, in which case defaults will be included)
     485             :   ///
     486             :   /// Example:
     487             :   ///
     488             :   /// ```
     489             :   /// await sendRequest(
     490             :   ///   baseUrl.asUri + 'token' & await defaultParams & {'a': 1},
     491             :   ///   headers: await defaultHeaders & {'a': 'b'},
     492             :   ///   onSuccess: (data) => data['token'] as String,
     493             :   /// );
     494             :   /// ```
     495             :   ///
     496             :   ///ignore: comment_references
     497             :   /// To build the URI you can use [String.asUri], [Uri.+] and [Uri.&].
     498             :   ///
     499             :   /// To merge headers and params with their defaults you can use the helper
     500             :   /// [Map<String, dynamic>.&].
     501             :   ///
     502             :   /// In addition, [onSuccess] is supplied to post-process the
     503             :   /// data in JSON format. Deserialization and initialization
     504             :   /// typically occur in this function.
     505             :   ///
     506             :   /// [onError] can also be supplied to override [_RemoteAdapter.onError].
     507             :   @protected
     508             :   @visibleForTesting
     509           6 :   Future<R?> sendRequest<R>(
     510             :     final Uri uri, {
     511             :     DataRequestMethod method = DataRequestMethod.GET,
     512             :     Map<String, String>? headers,
     513             :     String? body,
     514             :     _OnSuccessGeneric<R>? onSuccess,
     515             :     _OnErrorGeneric<R>? onError,
     516             :     bool omitDefaultParams = false,
     517             :     DataRequestLabel? label,
     518             :   }) async {
     519             :     // defaults
     520           4 :     headers ??= await defaultHeaders;
     521             :     final params =
     522          14 :         omitDefaultParams ? <String, dynamic>{} : await defaultParams;
     523             : 
     524           6 :     label ??= DataRequestLabel('custom', type: internalType);
     525           0 :     onSuccess ??= this.onSuccess;
     526           3 :     onError ??= this.onError;
     527             : 
     528             :     http.Response? response;
     529             :     Object? data;
     530             :     Object? error;
     531             :     StackTrace? stackTrace;
     532             : 
     533          24 :     final client = _isTesting ? read(httpClientProvider)! : httpClient;
     534             : 
     535           6 :     log(label,
     536          20 :         'requesting${_logLevel > 1 ? ' [HTTP ${method.toShortString()}] $uri' : ''}');
     537             : 
     538             :     try {
     539          18 :       final request = http.Request(method.toShortString(), uri & params);
     540          12 :       request.headers.addAll(headers);
     541             :       if (body != null) {
     542           6 :         request.body = body;
     543             :       }
     544          12 :       final stream = await client.send(request);
     545          12 :       response = await http.Response.fromStream(stream);
     546             :     } catch (err, stack) {
     547             :       error = err;
     548             :       stackTrace = stack;
     549             :     } finally {
     550           6 :       client.close();
     551             :     }
     552             : 
     553             :     // response handling
     554             : 
     555             :     try {
     556          12 :       if (response?.body.isNotEmpty ?? false) {
     557          10 :         data = json.decode(response!.body);
     558             :       }
     559           2 :     } on FormatException catch (e) {
     560             :       error = e;
     561             :     }
     562             : 
     563           6 :     final code = response?.statusCode;
     564             : 
     565          12 :     if (error == null && code != null && code >= 200 && code < 300) {
     566           6 :       return onSuccess(data, label);
     567             :     } else {
     568           3 :       if (isOfflineError(error)) {
     569             :         // queue a new operation if:
     570             :         //  - this is a network error and we're offline
     571             :         //  - the request was not a find
     572           2 :         if (method != DataRequestMethod.GET) {
     573           1 :           OfflineOperation<T>(
     574           2 :             httpRequest: '${method.toShortString()} $uri',
     575             :             label: label,
     576             :             body: body,
     577             :             headers: headers,
     578             :             onSuccess: onSuccess as _OnSuccessGeneric<T>,
     579             :             onError: onError as _OnErrorGeneric<T>,
     580             :             adapter: this as RemoteAdapter<T>,
     581           1 :           ).add();
     582             :         }
     583             : 
     584             :         // wrap error in an OfflineException
     585           2 :         final offlineException = OfflineException(error: error!);
     586             : 
     587             :         // call error handler but do not return it
     588             :         // (this gives the user the chance to present
     589             :         // a UI element to retry fetching, for example)
     590           2 :         onError(offlineException, label);
     591             : 
     592             :         // instead return a fallback model from local storage
     593           2 :         switch (label.kind) {
     594           2 :           case 'findAll':
     595           1 :             return findAll(remote: false) as Future<R?>;
     596           2 :           case 'findOne':
     597           1 :           case 'save':
     598           2 :             return label.model as R?;
     599             :           default:
     600             :             return null;
     601             :         }
     602             :       }
     603             : 
     604             :       // if it was not a network error
     605             :       // remove all operations with this request
     606           2 :       OfflineOperation.remove(label, this as RemoteAdapter<T>);
     607             : 
     608           2 :       final e = DataException(error ?? data!,
     609             :           stackTrace: stackTrace, statusCode: code);
     610           9 :       log(label, '$e${_logLevel > 1 ? ' $uri' : ''}');
     611           4 :       if (_logLevel > 1 && stackTrace != null) {
     612           0 :         log(label, stackTrace.toString());
     613             :       }
     614           4 :       return await onError(e, label);
     615             :     }
     616             :   }
     617             : 
     618           6 :   FutureOr<R?> onSuccess<R>(Object? data, DataRequestLabel label) async {
     619             :     // remove all operations with this label
     620           6 :     OfflineOperation.remove(label, this as RemoteAdapter);
     621             : 
     622          12 :     if (label.kind == 'save') {
     623           5 :       if (label.model == null) {
     624             :         return null;
     625             :       }
     626             :       if (data == null) {
     627             :         // return original model if response was empty
     628           4 :         return label.model as R?;
     629             :       }
     630             : 
     631             :       // deserialize already inits models
     632             :       // if model had a key already, reuse it
     633           4 :       final deserialized = await deserialize(data as Map<String, dynamic>);
     634           6 :       final model = deserialized.model!.withKeyOf(label.model as T);
     635             : 
     636           2 :       model.saveLocal();
     637           2 :       log(label, 'saved in local storage and remote');
     638             : 
     639             :       return model as R?;
     640             :     }
     641             : 
     642          10 :     if (label.kind == 'delete') {
     643           3 :       log(label, 'deleted in local storage and remote');
     644             :       return null;
     645             :     }
     646             : 
     647          10 :     final deserialized = await deserialize(data);
     648             : 
     649          10 :     final isFindAll = label.kind.startsWith('findAll');
     650          10 :     final isFindOne = label.kind.startsWith('findOne');
     651          10 :     final isCustom = label.kind == 'custom';
     652             : 
     653           2 :     if (isFindAll || (isCustom && deserialized.model == null)) {
     654          15 :       for (final model in [...deserialized.models, ...deserialized.included]) {
     655          12 :         model._remoteAdapter.localAdapter.save(model._key!, model);
     656             :       }
     657           4 :       deserialized._log(this as RemoteAdapter, label);
     658             : 
     659           4 :       return deserialized.models as R?;
     660             :     }
     661             : 
     662           2 :     if (isFindOne || (isCustom && deserialized.model != null)) {
     663          16 :       for (final model in [...deserialized.models, ...deserialized.included]) {
     664          16 :         model._remoteAdapter.localAdapter.save(model._key!, model);
     665             :       }
     666           4 :       deserialized._log(this as RemoteAdapter, label);
     667             : 
     668           4 :       return deserialized.model as R?;
     669             :     }
     670             : 
     671             :     return null;
     672             :   }
     673             : 
     674             :   /// Implements global request error handling.
     675             :   ///
     676             :   /// Defaults to throw [e] unless it is an HTTP 404
     677             :   /// or an `OfflineException`.
     678             :   ///
     679             :   /// NOTE: `onError` arguments throughout the API are used
     680             :   /// to override this default behavior.
     681           3 :   FutureOr<R?> onError<R>(
     682             :     DataException e,
     683             :     DataRequestLabel? label,
     684             :   ) {
     685           9 :     if (e.statusCode == 404 || e is OfflineException) {
     686             :       return null;
     687             :     }
     688             :     throw e;
     689             :   }
     690             : 
     691          10 :   void log(DataRequestLabel label, String message, {int logLevel = 1}) {
     692          20 :     if (_logLevel >= logLevel) {
     693           1 :       final now = DateTime.now();
     694             :       final timestamp =
     695           7 :           '${now.second.toString().padLeft(2, '0')}:${now.millisecond.toString().padLeft(3, '0')}';
     696           5 :       print('$timestamp ${' ' * label.indentation * 2}[$label] $message');
     697             :     }
     698             :   }
     699             : 
     700             :   /// After model initialization hook
     701          11 :   @protected
     702             :   void onModelInitialized(T model) {}
     703             : 
     704             :   // offline
     705             : 
     706             :   /// Determines whether [error] was an offline error.
     707           3 :   @protected
     708             :   @visibleForTesting
     709             :   bool isOfflineError(Object? error) {
     710             :     // timeouts via http's `connectionTimeout` are
     711             :     // also socket exceptions
     712             :     // we check the exception like this in order not to import `dart:io`
     713           3 :     final err = error.toString();
     714           3 :     return err.startsWith('SocketException') ||
     715           3 :         err.startsWith('Connection closed before full header was received') ||
     716           3 :         err.startsWith('HandshakeException');
     717             :   }
     718             : 
     719           1 :   @protected
     720             :   @visibleForTesting
     721             :   @nonVirtual
     722             :   Set<OfflineOperation<T>> get offlineOperations {
     723           2 :     final node = graph._getNode(_offlineAdapterKey, orAdd: true)!;
     724           1 :     return node.entries
     725           2 :         .map((e) {
     726             :           // extract type from e.g. _offline:findOne/users#3@d7bcc9
     727           3 :           final label = DataRequestLabel.parse(e.key.denamespace());
     728           3 :           if (label.type == internalType) {
     729             :             // get first edge value
     730           3 :             final map = json.decode(e.value.first) as Map<String, dynamic>;
     731           1 :             return OfflineOperation<T>.fromJson(
     732             :                 label, map, this as RemoteAdapter<T>);
     733             :           }
     734             :         })
     735           1 :         .filterNulls
     736           1 :         .toSet();
     737             :   }
     738             : 
     739           6 :   Object? _resolveId(Object obj) {
     740           9 :     return obj is T ? obj.id : obj;
     741             :   }
     742             : 
     743           6 :   @protected
     744             :   @visibleForTesting
     745             :   @nonVirtual
     746             :   String? keyForModelOrId(Object model) {
     747           6 :     if (model is T) {
     748           5 :       return model._key;
     749             :     } else {
     750           3 :       final id = _resolveId(model);
     751             :       if (id != null) {
     752           9 :         return graph.getKeyForId(internalType, id,
     753           3 :             keyIfAbsent: DataHelpers.generateKey<T>())!;
     754             :       } else {
     755             :         return null;
     756             :       }
     757             :     }
     758             :   }
     759             : 
     760           6 :   bool get _isTesting {
     761          18 :     return read(httpClientProvider) != null;
     762             :   }
     763             : }
     764             : 
     765             : /// When this provider is non-null it will override
     766             : /// all [_RemoteAdapter.httpClient] overrides;
     767             : /// it is useful for providing a mock client for testing
     768          33 : final httpClientProvider = Provider<http.Client?>((_) => null);

Generated by: LCOV version 1.15