LCOV - code coverage report
Current view: top level - repository - remote_adapter.dart (source / functions) Hit Total Coverage
Test: lcov.info Lines: 155 160 96.9 %
Date: 2021-06-18 12:41:16 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 [_RemoteAdapterWatch.watchOne]
      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, this.title, this.completed = false});
      26             : /// }
      27             : /// ```
      28             : class RemoteAdapter<T extends DataModel<T>> = _RemoteAdapter<T>
      29             :     with
      30             :         _RemoteAdapterSerialization<T>,
      31             :         _RemoteAdapterOffline<T>,
      32             :         _RemoteAdapterWatch<T>;
      33             : 
      34             : abstract class _RemoteAdapter<T extends DataModel<T>> with _Lifecycle {
      35           1 :   @protected
      36             :   _RemoteAdapter(this.localAdapter);
      37             : 
      38             :   @protected
      39             :   @visibleForTesting
      40             :   final LocalAdapter<T> localAdapter;
      41             : 
      42             :   /// A [GraphNotifier] instance also available to adapters
      43           1 :   @protected
      44           2 :   GraphNotifier get graph => localAdapter.graph;
      45             : 
      46             :   /// All adapters for the relationship subgraph of [T] and their relationships.
      47             :   ///
      48             :   /// This [Map] is typically required when initializing new models, and passed as-is.
      49             :   @protected
      50             :   late final Map<String, RemoteAdapter> adapters;
      51             : 
      52             :   late final bool _remote;
      53             :   late final bool _verbose;
      54             : 
      55             :   /// Give adapter subclasses access to the dependency injection system
      56             :   @nonVirtual
      57             :   @protected
      58             :   late final ProviderReference ref;
      59             : 
      60             :   /// INTERNAL: DO NOT USE OR ELSE THINGS WILL BREAK
      61           1 :   @visibleForTesting
      62             :   @protected
      63           1 :   String get internalType => DataHelpers.getType<T>();
      64             : 
      65             :   /// The pluralized and downcased [DataHelpers.getType<T>] version of type [T]
      66             :   /// by default.
      67             :   ///
      68             :   /// Example: [T] as `Post` has a [type] of `posts`.
      69           1 :   @visibleForTesting
      70             :   @protected
      71           1 :   String get type => internalType;
      72             : 
      73             :   /// Returns the base URL for this type [T].
      74             :   ///
      75             :   /// Typically used in a generic adapter (i.e. one shared by all types)
      76             :   /// so it should be e.g. `http://jsonplaceholder.typicode.com/`
      77             :   ///
      78             :   /// For specific paths to this type [T], see [urlForFindAll], [urlForFindOne], etc
      79           1 :   @protected
      80           1 :   String get baseUrl => throw UnsupportedError('Please override baseUrl');
      81             : 
      82             :   /// Returns URL for [findAll]. Defaults to [type].
      83           1 :   @protected
      84           2 :   String urlForFindAll(Map<String, dynamic> params) => '$type';
      85             : 
      86             :   /// Returns HTTP method for [findAll]. Defaults to `GET`.
      87           1 :   @protected
      88             :   DataRequestMethod methodForFindAll(Map<String, dynamic> params) =>
      89             :       DataRequestMethod.GET;
      90             : 
      91             :   /// Returns URL for [findOne]. Defaults to [type]/[id].
      92           1 :   @protected
      93           2 :   String urlForFindOne(id, Map<String, dynamic> params) => '$type/$id';
      94             : 
      95             :   /// Returns HTTP method for [findOne]. Defaults to `GET`.
      96           1 :   @protected
      97             :   DataRequestMethod methodForFindOne(id, Map<String, dynamic> params) =>
      98             :       DataRequestMethod.GET;
      99             : 
     100             :   /// Returns URL for [save]. Defaults to [type]/[id] (if [id] is present).
     101           1 :   @protected
     102             :   String urlForSave(id, Map<String, dynamic> params) =>
     103           3 :       id != null ? '$type/$id' : type;
     104             : 
     105             :   /// Returns HTTP method for [save]. Defaults to `PATCH` if [id] is present,
     106             :   /// or `POST` otherwise.
     107           1 :   @protected
     108             :   DataRequestMethod methodForSave(id, Map<String, dynamic> params) =>
     109             :       id != null ? DataRequestMethod.PATCH : DataRequestMethod.POST;
     110             : 
     111             :   /// Returns URL for [delete]. Defaults to [type]/[id].
     112           1 :   @protected
     113           2 :   String urlForDelete(id, Map<String, dynamic> params) => '$type/$id';
     114             : 
     115             :   /// Returns HTTP method for [delete]. Defaults to `DELETE`.
     116           1 :   @protected
     117             :   DataRequestMethod methodForDelete(id, Map<String, dynamic> params) =>
     118             :       DataRequestMethod.DELETE;
     119             : 
     120             :   /// A [Map] representing default HTTP query parameters. Defaults to empty.
     121             :   ///
     122             :   /// It can return a [Future], so that adapters overriding this method
     123             :   /// have a chance to call async methods.
     124             :   ///
     125             :   /// Example:
     126             :   /// ```
     127             :   /// @override
     128             :   /// FutureOr<Map<String, dynamic>> get defaultParams async {
     129             :   ///   final token = await _localStorage.get('token');
     130             :   ///   return await super.defaultParams..addAll({'token': token});
     131             :   /// }
     132             :   /// ```
     133           1 :   @protected
     134           1 :   FutureOr<Map<String, dynamic>> get defaultParams => {};
     135             : 
     136             :   /// A [Map] representing default HTTP headers.
     137             :   ///
     138             :   /// Initial default is: `{'Content-Type': 'application/json'}`.
     139             :   ///
     140             :   /// It can return a [Future], so that adapters overriding this method
     141             :   /// have a chance to call async methods.
     142             :   ///
     143             :   /// Example:
     144             :   /// ```
     145             :   /// @override
     146             :   /// FutureOr<Map<String, String>> get defaultHeaders async {
     147             :   ///   final token = await _localStorage.get('token');
     148             :   ///   return await super.defaultHeaders..addAll({'Authorization': token});
     149             :   /// }
     150             :   /// ```
     151           1 :   @protected
     152             :   FutureOr<Map<String, String>> get defaultHeaders =>
     153           1 :       {'Content-Type': 'application/json'};
     154             : 
     155             :   // lifecycle methods
     156             : 
     157             :   @mustCallSuper
     158           1 :   Future<void> onInitialized() async {}
     159             : 
     160             :   @mustCallSuper
     161           1 :   Future<RemoteAdapter<T>> initialize(
     162             :       {bool? remote,
     163             :       bool? verbose,
     164             :       required Map<String, RemoteAdapter> adapters,
     165             :       required ProviderReference ref}) async {
     166           1 :     if (isInitialized) return this as RemoteAdapter<T>;
     167           1 :     this.adapters = adapters;
     168           1 :     this.ref = ref;
     169             : 
     170             :     // set defaults
     171           1 :     _remote = remote ?? true;
     172           1 :     _verbose = verbose ?? true;
     173             : 
     174           3 :     await localAdapter.initialize();
     175             : 
     176             :     // hook for clients
     177           2 :     await onInitialized();
     178             : 
     179             :     return this as RemoteAdapter<T>;
     180             :   }
     181             : 
     182           1 :   @override
     183           2 :   bool get isInitialized => localAdapter.isInitialized;
     184             : 
     185           1 :   @override
     186             :   void dispose() {
     187           2 :     localAdapter.dispose();
     188             :   }
     189             : 
     190           1 :   void _assertInit() {
     191             :     assert(isInitialized, true);
     192             :   }
     193             : 
     194             :   // serialization interface
     195             : 
     196             :   /// Returns a [DeserializedData] object when deserializing a given [data].
     197             :   ///
     198             :   /// [key] can be used to supply a specific `key` when deserializing ONE model.
     199             :   @protected
     200             :   @visibleForTesting
     201             :   DeserializedData<T, DataModel> deserialize(Object data, {String key});
     202             : 
     203             :   /// Returns a serialized version of a model of [T],
     204             :   /// as a [Map<String, dynamic>] ready to be JSON-encoded.
     205             :   @protected
     206             :   @visibleForTesting
     207             :   Map<String, dynamic> serialize(T model);
     208             : 
     209             :   // caching
     210             : 
     211             :   /// Returns whether calling [findAll] should trigger a remote call.
     212             :   ///
     213             :   /// Meant to be overriden. Defaults to [remote].
     214           1 :   @protected
     215             :   bool shouldLoadRemoteAll(
     216             :     bool remote,
     217             :     Map<String, dynamic> params,
     218             :     Map<String, String> headers,
     219             :   ) =>
     220             :       remote;
     221             : 
     222             :   /// Returns whether calling [findOne] should initiate an HTTP call.
     223             :   ///
     224             :   /// Meant to be overriden. Defaults to [remote].
     225           1 :   @protected
     226             :   bool shouldLoadRemoteOne(
     227             :     dynamic id,
     228             :     bool remote,
     229             :     Map<String, dynamic> params,
     230             :     Map<String, String> headers,
     231             :   ) =>
     232             :       remote;
     233             : 
     234             :   // remote implementation
     235             : 
     236             :   @protected
     237             :   @visibleForTesting
     238           1 :   Future<List<T>> findAll({
     239             :     bool? remote,
     240             :     Map<String, dynamic>? params,
     241             :     Map<String, String>? headers,
     242             :     bool? syncLocal,
     243             :     bool Function(T)? filterLocal,
     244             :     OnData<List<T>>? onSuccess,
     245             :     OnDataError<List<T>>? onError,
     246             :   }) async {
     247           1 :     _assertInit();
     248           1 :     remote ??= _remote;
     249             :     syncLocal ??= false;
     250           3 :     params = await defaultParams & params;
     251           3 :     headers = await defaultHeaders & headers;
     252           1 :     filterLocal ??= (_) => true;
     253             : 
     254           1 :     if (!shouldLoadRemoteAll(remote, params, headers)) {
     255             :       final models =
     256           4 :           localAdapter.findAll().where(filterLocal).toImmutableList();
     257           1 :       models.map((m) => m._initialize(adapters, save: true));
     258             :       return models;
     259             :     }
     260             : 
     261           2 :     final result = await sendRequest(
     262           5 :       baseUrl.asUri / urlForFindAll(params) & params,
     263           1 :       method: methodForFindAll(params),
     264             :       headers: headers,
     265             :       requestType: DataRequestType.findAll,
     266           1 :       key: internalType,
     267           1 :       onSuccess: (data) async {
     268             :         if (syncLocal!) {
     269           3 :           await localAdapter.clear();
     270             :         }
     271             :         final models = data != null
     272           1 :             ? deserialize(data as Object)
     273           1 :                 .models
     274           1 :                 .where(filterLocal!)
     275           1 :                 .toImmutableList()
     276           1 :             : <T>[];
     277           0 :         return onSuccess?.call(models) ?? models;
     278             :       },
     279             :       onError: onError,
     280             :     );
     281           1 :     return result ?? <T>[];
     282             :   }
     283             : 
     284             :   @protected
     285             :   @visibleForTesting
     286           1 :   Future<T?> findOne(
     287             :     final dynamic model, {
     288             :     bool? remote,
     289             :     Map<String, dynamic>? params,
     290             :     Map<String, String>? headers,
     291             :     OnData<T?>? onSuccess,
     292             :     OnDataError<T?>? onError,
     293             :   }) async {
     294           1 :     _assertInit();
     295             :     if (model == null) {
     296           1 :       throw AssertionError('Model must be not null');
     297             :     }
     298           1 :     remote ??= _remote;
     299             : 
     300           3 :     params = await defaultParams & params;
     301           3 :     headers = await defaultHeaders & headers;
     302             : 
     303           1 :     final id = _resolveId(model);
     304             : 
     305           1 :     if (!shouldLoadRemoteOne(id, remote, params, headers)) {
     306           3 :       final key = graph.getKeyForId(internalType, id,
     307           2 :           keyIfAbsent: model is T ? model._key : null);
     308             :       if (key == null) {
     309             :         return null;
     310             :       }
     311           2 :       final newModel = localAdapter.findOne(key);
     312           2 :       newModel?._initialize(adapters, save: true);
     313             :       return newModel;
     314             :     }
     315             : 
     316           2 :     return await sendRequest(
     317           5 :       baseUrl.asUri / urlForFindOne(id, params) & params,
     318           1 :       method: methodForFindOne(id, params),
     319             :       headers: headers,
     320             :       requestType: DataRequestType.findOne,
     321           2 :       key: StringUtils.typify(internalType, id),
     322           1 :       onSuccess: (data) {
     323             :         final model = data != null
     324           2 :             ? deserialize(data as Map<String, dynamic>).model
     325             :             : null;
     326           0 :         return onSuccess?.call(model) ?? model;
     327             :       },
     328             :       onError: onError,
     329             :     );
     330             :   }
     331             : 
     332             :   @protected
     333             :   @visibleForTesting
     334           1 :   Future<T> save(
     335             :     T model, {
     336             :     bool? remote,
     337             :     Map<String, dynamic>? params,
     338             :     Map<String, String>? headers,
     339             :     OnData<T>? onSuccess,
     340             :     OnDataError<T>? onError,
     341             :   }) async {
     342           1 :     _assertInit();
     343           1 :     remote ??= _remote;
     344             : 
     345           3 :     params = await defaultParams & params;
     346           3 :     headers = await defaultHeaders & headers;
     347             : 
     348             :     // we ignore the `init` argument here as
     349             :     // saving locally requires initializing
     350           2 :     model._initialize(adapters, save: true);
     351             : 
     352           1 :     if (remote == false) {
     353             :       return model;
     354             :     }
     355             : 
     356           2 :     final body = json.encode(serialize(model));
     357             : 
     358           2 :     final result = await sendRequest(
     359           6 :       baseUrl.asUri / urlForSave(model.id, params) & params,
     360           2 :       method: methodForSave(model.id, params),
     361             :       headers: headers,
     362             :       body: body,
     363             :       requestType: DataRequestType.save,
     364           1 :       key: model._key,
     365           1 :       onSuccess: (data) {
     366             :         T _model;
     367             :         if (data == null) {
     368             :           // return "old" model if response was empty
     369           2 :           _model = model._initialize(adapters, save: true);
     370             :         } else {
     371             :           // deserialize already inits models
     372             :           // if model had a key already, reuse it
     373             :           final _newModel =
     374           2 :               deserialize(data as Map<String, dynamic>, key: model._key!)
     375           1 :                   .model!;
     376             : 
     377             :           // in the unlikely case where supplied key couldn't be used
     378             :           // ensure "old" copy of model carries the updated key
     379           4 :           if (model._key != null && model._key != _newModel._key) {
     380           3 :             graph.removeKey(model._key!);
     381           2 :             model._key = _newModel._key;
     382             :           }
     383             :           _model = _newModel;
     384             :         }
     385           1 :         return onSuccess?.call(_model) ?? _model;
     386             :       },
     387             :       onError: onError,
     388             :     );
     389             :     return result ?? model;
     390             :   }
     391             : 
     392             :   @protected
     393             :   @visibleForTesting
     394           1 :   Future<void> delete(
     395             :     dynamic model, {
     396             :     bool? remote,
     397             :     Map<String, dynamic>? params,
     398             :     Map<String, String>? headers,
     399             :     OnData<void>? onSuccess,
     400             :     OnDataError<void>? onError,
     401             :   }) async {
     402           1 :     _assertInit();
     403           1 :     remote ??= _remote;
     404             : 
     405           3 :     params = await defaultParams & params;
     406           3 :     headers = await defaultHeaders & headers;
     407             : 
     408           1 :     final id = _resolveId(model);
     409           1 :     final key = _keyForModel(model);
     410             : 
     411             :     if (key != null) {
     412           3 :       await localAdapter.delete(key);
     413             :     }
     414             : 
     415             :     if (remote) {
     416           2 :       return await sendRequest(
     417           5 :         baseUrl.asUri / urlForDelete(id, params) & params,
     418           1 :         method: methodForDelete(id, params),
     419             :         headers: headers,
     420             :         requestType: DataRequestType.delete,
     421           2 :         key: StringUtils.typify(internalType, id),
     422             :         onSuccess: onSuccess,
     423             :         onError: onError,
     424             :       );
     425             :     }
     426             :   }
     427             : 
     428           1 :   @protected
     429             :   @visibleForTesting
     430           2 :   Future<void> clear() => localAdapter.clear();
     431             : 
     432             :   // http
     433             : 
     434             :   /// An [http.Client] used to make an HTTP request.
     435             :   ///
     436             :   /// This getter returns a new client every time
     437             :   /// as by default they are used once and then closed.
     438           0 :   @protected
     439             :   @visibleForTesting
     440           0 :   http.Client get httpClient => http.Client();
     441             : 
     442             :   /// The function used to perform an HTTP request and return an [R].
     443             :   ///
     444             :   /// **IMPORTANT**:
     445             :   ///  - [uri] takes the FULL `Uri` including query parameters
     446             :   ///  - [headers] does NOT include ANY defaults such as [defaultHeaders]
     447             :   ///  (unless you omit the argument, in which case defaults will be included)
     448             :   ///
     449             :   /// Example:
     450             :   ///
     451             :   /// ```
     452             :   /// await sendRequest(
     453             :   ///   baseUrl.asUri + 'token' & await defaultParams & {'a': 1},
     454             :   ///   headers: await defaultHeaders & {'a': 'b'},
     455             :   ///   onSuccess: (data) => data['token'] as String,
     456             :   /// );
     457             :   /// ```
     458             :   ///
     459             :   ///ignore: comment_references
     460             :   /// To build the URI you can use [String.asUri], [Uri.+] and [Uri.&].
     461             :   ///
     462             :   /// To merge headers and params with their defaults you can use the helper
     463             :   /// [Map<String, dynamic>.&].
     464             :   ///
     465             :   /// In addition, [onSuccess] is supplied to post-process the
     466             :   /// data in JSON format. Deserialization and initialization
     467             :   /// typically occur in this function.
     468             :   ///
     469             :   /// [onError] can also be supplied to override [_RemoteAdapter.onError].
     470             :   @protected
     471             :   @visibleForTesting
     472           1 :   FutureOr<R?> sendRequest<R>(
     473             :     final Uri uri, {
     474             :     DataRequestMethod method = DataRequestMethod.GET,
     475             :     Map<String, String>? headers,
     476             :     String? body,
     477             :     String? key,
     478             :     OnRawData<R>? onSuccess,
     479             :     OnDataError<R>? onError,
     480             :     DataRequestType requestType = DataRequestType.adhoc,
     481             :     bool omitDefaultParams = false,
     482             :   }) async {
     483             :     // callbacks
     484           0 :     onError ??= this.onError as OnDataError<R>;
     485             : 
     486           2 :     headers ??= await defaultHeaders;
     487             :     final _params =
     488           3 :         omitDefaultParams ? <String, dynamic>{} : await defaultParams;
     489             : 
     490             :     http.Response? response;
     491             :     Object? data;
     492             :     Object? error;
     493             :     StackTrace? stackTrace;
     494             : 
     495             :     try {
     496           3 :       final request = http.Request(method.toShortString(), uri & _params);
     497           2 :       request.headers.addAll(headers);
     498             :       if (body != null) {
     499           1 :         request.body = body;
     500             :       }
     501           3 :       final stream = await httpClient.send(request);
     502           2 :       response = await http.Response.fromStream(stream);
     503             :     } catch (err, stack) {
     504             :       error = err;
     505             :       stackTrace = stack;
     506             :     } finally {
     507           2 :       httpClient.close();
     508             :     }
     509             : 
     510             :     // response handling
     511             : 
     512             :     try {
     513           2 :       if (response?.body.isNotEmpty ?? false) {
     514           2 :         data = json.decode(response!.body);
     515             :       }
     516           1 :     } on FormatException catch (e) {
     517             :       error = e;
     518             :     }
     519             : 
     520           1 :     final code = response?.statusCode;
     521             : 
     522           1 :     if (_verbose) {
     523           1 :       print(
     524           3 :           '[flutter_data] [$internalType] ${method.toShortString()} $uri [HTTP ${code ?? ''}]${body != null ? '\n -> body:\n $body' : ''}');
     525             :     }
     526             : 
     527           2 :     if (error == null && code != null && code >= 200 && code < 300) {
     528           2 :       return await onSuccess?.call(data);
     529             :     } else {
     530           1 :       final e = DataException(error ?? data!,
     531             :           stackTrace: stackTrace, statusCode: code);
     532             : 
     533           1 :       if (_verbose) {
     534           3 :         print('[flutter_data] [$internalType] Error: $e');
     535             :       }
     536           2 :       return await onError(e);
     537             :     }
     538             :   }
     539             : 
     540             :   /// Implements global request error handling.
     541             :   ///
     542             :   /// Defaults to throw [e] unless it is an HTTP 404
     543             :   /// or an `OfflineException`.
     544             :   ///
     545             :   /// NOTE: `onError` arguments throughout the API are used
     546             :   /// to override this default behavior.
     547           1 :   @protected
     548             :   @visibleForTesting
     549             :   FutureOr<R?> onError<R>(DataException e) {
     550           3 :     if (e.statusCode == 404 || e is OfflineException) {
     551             :       return null;
     552             :     }
     553             :     throw e;
     554             :   }
     555             : 
     556             :   /// Initializes [model] making it ready to use with [DataModel] extensions.
     557             :   ///
     558             :   /// Optionally provide [key]. Use [save] to persist in local storage.
     559           1 :   @protected
     560             :   @visibleForTesting
     561             :   T initializeModel(T model, {String? key, bool save = false}) {
     562           2 :     return model._initialize(adapters, key: key, save: save);
     563             :   }
     564             : 
     565           1 :   String? _resolveId(dynamic model) {
     566           2 :     final id = model is T ? model.id : model;
     567           1 :     return id?.toString();
     568             :   }
     569             : 
     570           1 :   String? _keyForModel(dynamic model) {
     571           1 :     final id = _resolveId(model);
     572           3 :     return graph.getKeyForId(internalType, id,
     573           2 :         keyIfAbsent: model is T ? model._key : null);
     574             :   }
     575             : }
     576             : 
     577             : /// A utility class used to return deserialized main [models] AND [included] models.
     578             : class DeserializedData<T, I> {
     579           1 :   const DeserializedData(this.models, {this.included = const []});
     580             :   final List<T> models;
     581             :   final List<I> included;
     582           3 :   T? get model => models.singleOrNull;
     583             : }
     584             : 
     585             : // ignore: constant_identifier_names
     586           2 : enum DataRequestMethod { GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE }
     587             : 
     588             : extension _ToStringX on DataRequestMethod {
     589           4 :   String toShortString() => toString().split('.').last;
     590             : }
     591             : 
     592             : typedef OnData<R> = FutureOr<R> Function(R);
     593             : typedef OnRawData<R> = FutureOr<R?> Function(dynamic);
     594             : typedef OnDataError<R> = FutureOr<R?> Function(DataException);
     595             : 
     596             : // ignore: constant_identifier_names
     597           2 : enum DataRequestType {
     598             :   findAll,
     599             :   findOne,
     600             :   save,
     601             :   delete,
     602             :   adhoc,
     603             : }
     604             : 
     605             : extension _DataRequestTypeX on DataRequestType {
     606           4 :   String toShortString() => toString().split('.').last;
     607             : }
     608             : 
     609           1 : DataRequestType _getDataRequestType(String type) =>
     610           4 :     DataRequestType.values.singleWhere((_) => _.toShortString() == type);

Generated by: LCOV version 1.15