diff --git a/CHANGELOG.md b/CHANGELOG.md index b633733b..9a76f1f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 9.0.0 + +* Add offline support + ## 8.3.0 * Fix: back navigation bringing back web browser after OAuth session creation diff --git a/README.md b/README.md index d8f36294..45c77b4f 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,12 @@ [![pub package](https://img.shields.io/pub/v/appwrite?style=flat-square)](https://pub.dartlang.org/packages/appwrite) ![License](https://img.shields.io/github/license/appwrite/sdk-for-flutter.svg?style=flat-square) -![Version](https://img.shields.io/badge/api%20version-1.2.1-blue.svg?style=flat-square) +![Version](https://img.shields.io/badge/api%20version-1.3.0-blue.svg?style=flat-square) [![Build Status](https://img.shields.io/travis/com/appwrite/sdk-generator?style=flat-square)](https://travis-ci.com/appwrite/sdk-generator) [![Twitter Account](https://img.shields.io/twitter/follow/appwrite?color=00acee&label=twitter&style=flat-square)](https://twitter.com/appwrite) [![Discord](https://img.shields.io/discord/564160730845151244?label=discord&style=flat-square)](https://appwrite.io/discord) -**This SDK is compatible with Appwrite server version 1.2.x. For older versions, please check [previous releases](https://github.com/appwrite/sdk-for-flutter/releases).** +**This SDK is compatible with Appwrite server version 1.3.x. For older versions, please check [previous releases](https://github.com/appwrite/sdk-for-flutter/releases).** Appwrite is an open-source backend as a service server that abstract and simplify complex and repetitive development tasks behind a very simple to use REST API. Appwrite aims to help you develop your apps faster and in a more secure way. Use the Flutter SDK to integrate your app with the Appwrite server to easily start interacting with all of Appwrite backend APIs and tools. For full API documentation and tutorials go to [https://appwrite.io/docs](https://appwrite.io/docs) @@ -21,7 +21,7 @@ Add this to your package's `pubspec.yaml` file: ```yml dependencies: - appwrite: ^8.3.0 + appwrite: ^9.0.0 ``` You can install packages from the command line: diff --git a/example/pubspec.yaml b/example/pubspec.yaml index fc723083..785d4760 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,3 +1,3 @@ name: appwrite_example environment: - sdk: '>=2.17.0 <3.0.0' \ No newline at end of file + sdk: ">=2.17.0 <3.0.0" diff --git a/lib/appwrite.dart b/lib/appwrite.dart index 10638312..2eb81657 100644 --- a/lib/appwrite.dart +++ b/lib/appwrite.dart @@ -1,6 +1,7 @@ library appwrite; import 'dart:async'; +import 'dart:math'; import 'dart:typed_data'; import 'src/enums.dart'; import 'src/service.dart'; @@ -8,19 +9,19 @@ import 'src/input_file.dart'; import 'models.dart' as models; import 'src/upload_progress.dart'; -export 'src/response.dart'; export 'src/client.dart'; export 'src/exception.dart'; +export 'src/input_file.dart'; export 'src/realtime.dart'; -export 'src/upload_progress.dart'; -export 'src/realtime_subscription.dart'; export 'src/realtime_message.dart'; -export 'src/input_file.dart'; +export 'src/realtime_subscription.dart'; +export 'src/response.dart'; +export 'src/upload_progress.dart'; -part 'query.dart'; +part 'id.dart'; part 'permission.dart'; +part 'query.dart'; part 'role.dart'; -part 'id.dart'; part 'services/account.dart'; part 'services/avatars.dart'; part 'services/databases.dart'; diff --git a/lib/id.dart b/lib/id.dart index e32f0018..f13a7bb6 100644 --- a/lib/id.dart +++ b/lib/id.dart @@ -1,13 +1,40 @@ part of appwrite; class ID { - ID._(); - - static String unique() { - return 'unique()'; - } + ID._(); + + // Generate a unique ID based on timestamp + // Recreated from https://www.php.net/manual/en/function.uniqid.php + static String _uniqid() { + final now = DateTime.now(); + final secondsSinceEpoch = (now.millisecondsSinceEpoch / 1000).floor(); + final msecs = now.microsecondsSinceEpoch - secondsSinceEpoch * 1000000; + return secondsSinceEpoch.toRadixString(16) + + msecs.toRadixString(16).padLeft(5, '0'); + } + + // Generate a unique ID with padding to have a longer ID + // Recreated from https://github.com/utopia-php/database/blob/main/src/Database/ID.php#L13 + static String _unique({int padding = 7}) { + String id = _uniqid(); + + if (padding > 0) { + StringBuffer sb = StringBuffer(); + for (var i = 0; i < padding; i++) { + sb.write(Random().nextInt(16).toRadixString(16)); + } - static String custom(String id) { - return id; + id += sb.toString(); } -} \ No newline at end of file + + return id; + } + + static String unique() { + return _unique(); + } + + static String custom(String id) { + return id; + } +} diff --git a/lib/services/account.dart b/lib/services/account.dart index 001278f7..c58831b8 100644 --- a/lib/services/account.dart +++ b/lib/services/account.dart @@ -18,7 +18,21 @@ class Account extends Service { 'content-type': 'application/json', }; - final res = await client.call(HttpMethod.get, path: path, params: params, headers: headers); + final cacheModel = '/account'; + final cacheKey = 'current'; + final cacheResponseIdKey = '\$id'; + final cacheResponseContainerKey = ''; + + final res = await client.call( + HttpMethod.get, + path: path, + params: params, + headers: headers, + cacheModel: cacheModel, + cacheKey: cacheKey, + cacheResponseIdKey: cacheResponseIdKey, + cacheResponseContainerKey: cacheResponseContainerKey, + ); return models.Account.fromMap(res.data); @@ -47,7 +61,21 @@ class Account extends Service { 'content-type': 'application/json', }; - final res = await client.call(HttpMethod.post, path: path, params: params, headers: headers); + final cacheModel = ''; + final cacheKey = ''; + final cacheResponseIdKey = '\$id'; + final cacheResponseContainerKey = ''; + + final res = await client.call( + HttpMethod.post, + path: path, + params: params, + headers: headers, + cacheModel: cacheModel, + cacheKey: cacheKey, + cacheResponseIdKey: cacheResponseIdKey, + cacheResponseContainerKey: cacheResponseContainerKey, + ); return models.Account.fromMap(res.data); @@ -76,7 +104,21 @@ class Account extends Service { 'content-type': 'application/json', }; - final res = await client.call(HttpMethod.patch, path: path, params: params, headers: headers); + final cacheModel = '/account'; + final cacheKey = 'current'; + final cacheResponseIdKey = '\$id'; + final cacheResponseContainerKey = ''; + + final res = await client.call( + HttpMethod.patch, + path: path, + params: params, + headers: headers, + cacheModel: cacheModel, + cacheKey: cacheKey, + cacheResponseIdKey: cacheResponseIdKey, + cacheResponseContainerKey: cacheResponseContainerKey, + ); return models.Account.fromMap(res.data); @@ -100,7 +142,21 @@ class Account extends Service { 'content-type': 'application/json', }; - final res = await client.call(HttpMethod.post, path: path, params: params, headers: headers); + final cacheModel = ''; + final cacheKey = ''; + final cacheResponseIdKey = '\$id'; + final cacheResponseContainerKey = ''; + + final res = await client.call( + HttpMethod.post, + path: path, + params: params, + headers: headers, + cacheModel: cacheModel, + cacheKey: cacheKey, + cacheResponseIdKey: cacheResponseIdKey, + cacheResponseContainerKey: cacheResponseContainerKey, + ); return models.Jwt.fromMap(res.data); @@ -122,7 +178,21 @@ class Account extends Service { 'content-type': 'application/json', }; - final res = await client.call(HttpMethod.get, path: path, params: params, headers: headers); + final cacheModel = ''; + final cacheKey = ''; + final cacheResponseIdKey = '\$id'; + final cacheResponseContainerKey = 'logs'; + + final res = await client.call( + HttpMethod.get, + path: path, + params: params, + headers: headers, + cacheModel: cacheModel, + cacheKey: cacheKey, + cacheResponseIdKey: cacheResponseIdKey, + cacheResponseContainerKey: cacheResponseContainerKey, + ); return models.LogList.fromMap(res.data); @@ -143,7 +213,21 @@ class Account extends Service { 'content-type': 'application/json', }; - final res = await client.call(HttpMethod.patch, path: path, params: params, headers: headers); + final cacheModel = '/account'; + final cacheKey = 'current'; + final cacheResponseIdKey = '\$id'; + final cacheResponseContainerKey = ''; + + final res = await client.call( + HttpMethod.patch, + path: path, + params: params, + headers: headers, + cacheModel: cacheModel, + cacheKey: cacheKey, + cacheResponseIdKey: cacheResponseIdKey, + cacheResponseContainerKey: cacheResponseContainerKey, + ); return models.Account.fromMap(res.data); @@ -167,7 +251,21 @@ class Account extends Service { 'content-type': 'application/json', }; - final res = await client.call(HttpMethod.patch, path: path, params: params, headers: headers); + final cacheModel = '/account'; + final cacheKey = 'current'; + final cacheResponseIdKey = '\$id'; + final cacheResponseContainerKey = ''; + + final res = await client.call( + HttpMethod.patch, + path: path, + params: params, + headers: headers, + cacheModel: cacheModel, + cacheKey: cacheKey, + cacheResponseIdKey: cacheResponseIdKey, + cacheResponseContainerKey: cacheResponseContainerKey, + ); return models.Account.fromMap(res.data); @@ -193,7 +291,21 @@ class Account extends Service { 'content-type': 'application/json', }; - final res = await client.call(HttpMethod.patch, path: path, params: params, headers: headers); + final cacheModel = '/account'; + final cacheKey = 'current'; + final cacheResponseIdKey = '\$id'; + final cacheResponseContainerKey = ''; + + final res = await client.call( + HttpMethod.patch, + path: path, + params: params, + headers: headers, + cacheModel: cacheModel, + cacheKey: cacheKey, + cacheResponseIdKey: cacheResponseIdKey, + cacheResponseContainerKey: cacheResponseContainerKey, + ); return models.Account.fromMap(res.data); @@ -213,7 +325,21 @@ class Account extends Service { 'content-type': 'application/json', }; - final res = await client.call(HttpMethod.get, path: path, params: params, headers: headers); + final cacheModel = '/account/prefs'; + final cacheKey = 'current'; + final cacheResponseIdKey = '\$id'; + final cacheResponseContainerKey = ''; + + final res = await client.call( + HttpMethod.get, + path: path, + params: params, + headers: headers, + cacheModel: cacheModel, + cacheKey: cacheKey, + cacheResponseIdKey: cacheResponseIdKey, + cacheResponseContainerKey: cacheResponseContainerKey, + ); return models.Preferences.fromMap(res.data); @@ -236,7 +362,21 @@ class Account extends Service { 'content-type': 'application/json', }; - final res = await client.call(HttpMethod.patch, path: path, params: params, headers: headers); + final cacheModel = '/account/prefs'; + final cacheKey = 'current'; + final cacheResponseIdKey = '\$id'; + final cacheResponseContainerKey = ''; + + final res = await client.call( + HttpMethod.patch, + path: path, + params: params, + headers: headers, + cacheModel: cacheModel, + cacheKey: cacheKey, + cacheResponseIdKey: cacheResponseIdKey, + cacheResponseContainerKey: cacheResponseContainerKey, + ); return models.Account.fromMap(res.data); @@ -265,7 +405,21 @@ class Account extends Service { 'content-type': 'application/json', }; - final res = await client.call(HttpMethod.post, path: path, params: params, headers: headers); + final cacheModel = ''; + final cacheKey = ''; + final cacheResponseIdKey = '\$id'; + final cacheResponseContainerKey = ''; + + final res = await client.call( + HttpMethod.post, + path: path, + params: params, + headers: headers, + cacheModel: cacheModel, + cacheKey: cacheKey, + cacheResponseIdKey: cacheResponseIdKey, + cacheResponseContainerKey: cacheResponseContainerKey, + ); return models.Token.fromMap(res.data); @@ -297,7 +451,21 @@ class Account extends Service { 'content-type': 'application/json', }; - final res = await client.call(HttpMethod.put, path: path, params: params, headers: headers); + final cacheModel = ''; + final cacheKey = ''; + final cacheResponseIdKey = '\$id'; + final cacheResponseContainerKey = ''; + + final res = await client.call( + HttpMethod.put, + path: path, + params: params, + headers: headers, + cacheModel: cacheModel, + cacheKey: cacheKey, + cacheResponseIdKey: cacheResponseIdKey, + cacheResponseContainerKey: cacheResponseContainerKey, + ); return models.Token.fromMap(res.data); @@ -318,7 +486,21 @@ class Account extends Service { 'content-type': 'application/json', }; - final res = await client.call(HttpMethod.get, path: path, params: params, headers: headers); + final cacheModel = '/account/sessions'; + final cacheKey = ''; + final cacheResponseIdKey = '\$id'; + final cacheResponseContainerKey = 'sessions'; + + final res = await client.call( + HttpMethod.get, + path: path, + params: params, + headers: headers, + cacheModel: cacheModel, + cacheKey: cacheKey, + cacheResponseIdKey: cacheResponseIdKey, + cacheResponseContainerKey: cacheResponseContainerKey, + ); return models.SessionList.fromMap(res.data); @@ -339,7 +521,21 @@ class Account extends Service { 'content-type': 'application/json', }; - final res = await client.call(HttpMethod.delete, path: path, params: params, headers: headers); + final cacheModel = ''; + final cacheKey = ''; + final cacheResponseIdKey = '\$id'; + final cacheResponseContainerKey = ''; + + final res = await client.call( + HttpMethod.delete, + path: path, + params: params, + headers: headers, + cacheModel: cacheModel, + cacheKey: cacheKey, + cacheResponseIdKey: cacheResponseIdKey, + cacheResponseContainerKey: cacheResponseContainerKey, + ); return res.data; @@ -364,7 +560,21 @@ class Account extends Service { 'content-type': 'application/json', }; - final res = await client.call(HttpMethod.post, path: path, params: params, headers: headers); + final cacheModel = ''; + final cacheKey = ''; + final cacheResponseIdKey = '\$id'; + final cacheResponseContainerKey = ''; + + final res = await client.call( + HttpMethod.post, + path: path, + params: params, + headers: headers, + cacheModel: cacheModel, + cacheKey: cacheKey, + cacheResponseIdKey: cacheResponseIdKey, + cacheResponseContainerKey: cacheResponseContainerKey, + ); return models.Session.fromMap(res.data); @@ -390,7 +600,21 @@ class Account extends Service { 'content-type': 'application/json', }; - final res = await client.call(HttpMethod.post, path: path, params: params, headers: headers); + final cacheModel = ''; + final cacheKey = ''; + final cacheResponseIdKey = '\$id'; + final cacheResponseContainerKey = ''; + + final res = await client.call( + HttpMethod.post, + path: path, + params: params, + headers: headers, + cacheModel: cacheModel, + cacheKey: cacheKey, + cacheResponseIdKey: cacheResponseIdKey, + cacheResponseContainerKey: cacheResponseContainerKey, + ); return models.Session.fromMap(res.data); @@ -426,7 +650,21 @@ class Account extends Service { 'content-type': 'application/json', }; - final res = await client.call(HttpMethod.post, path: path, params: params, headers: headers); + final cacheModel = ''; + final cacheKey = ''; + final cacheResponseIdKey = '\$id'; + final cacheResponseContainerKey = ''; + + final res = await client.call( + HttpMethod.post, + path: path, + params: params, + headers: headers, + cacheModel: cacheModel, + cacheKey: cacheKey, + cacheResponseIdKey: cacheResponseIdKey, + cacheResponseContainerKey: cacheResponseContainerKey, + ); return models.Token.fromMap(res.data); @@ -458,7 +696,21 @@ class Account extends Service { 'content-type': 'application/json', }; - final res = await client.call(HttpMethod.put, path: path, params: params, headers: headers); + final cacheModel = ''; + final cacheKey = ''; + final cacheResponseIdKey = '\$id'; + final cacheResponseContainerKey = ''; + + final res = await client.call( + HttpMethod.put, + path: path, + params: params, + headers: headers, + cacheModel: cacheModel, + cacheKey: cacheKey, + cacheResponseIdKey: cacheResponseIdKey, + cacheResponseContainerKey: cacheResponseContainerKey, + ); return models.Session.fromMap(res.data); @@ -541,7 +793,21 @@ class Account extends Service { 'content-type': 'application/json', }; - final res = await client.call(HttpMethod.post, path: path, params: params, headers: headers); + final cacheModel = ''; + final cacheKey = ''; + final cacheResponseIdKey = '\$id'; + final cacheResponseContainerKey = ''; + + final res = await client.call( + HttpMethod.post, + path: path, + params: params, + headers: headers, + cacheModel: cacheModel, + cacheKey: cacheKey, + cacheResponseIdKey: cacheResponseIdKey, + cacheResponseContainerKey: cacheResponseContainerKey, + ); return models.Token.fromMap(res.data); @@ -567,7 +833,21 @@ class Account extends Service { 'content-type': 'application/json', }; - final res = await client.call(HttpMethod.put, path: path, params: params, headers: headers); + final cacheModel = ''; + final cacheKey = ''; + final cacheResponseIdKey = '\$id'; + final cacheResponseContainerKey = ''; + + final res = await client.call( + HttpMethod.put, + path: path, + params: params, + headers: headers, + cacheModel: cacheModel, + cacheKey: cacheKey, + cacheResponseIdKey: cacheResponseIdKey, + cacheResponseContainerKey: cacheResponseContainerKey, + ); return models.Session.fromMap(res.data); @@ -588,7 +868,21 @@ class Account extends Service { 'content-type': 'application/json', }; - final res = await client.call(HttpMethod.get, path: path, params: params, headers: headers); + final cacheModel = '/account/sessions'.replaceAll('{sessionId}', sessionId); + final cacheKey = sessionId; + final cacheResponseIdKey = '\$id'; + final cacheResponseContainerKey = ''; + + final res = await client.call( + HttpMethod.get, + path: path, + params: params, + headers: headers, + cacheModel: cacheModel, + cacheKey: cacheKey, + cacheResponseIdKey: cacheResponseIdKey, + cacheResponseContainerKey: cacheResponseContainerKey, + ); return models.Session.fromMap(res.data); @@ -610,7 +904,21 @@ class Account extends Service { 'content-type': 'application/json', }; - final res = await client.call(HttpMethod.patch, path: path, params: params, headers: headers); + final cacheModel = ''; + final cacheKey = ''; + final cacheResponseIdKey = '\$id'; + final cacheResponseContainerKey = ''; + + final res = await client.call( + HttpMethod.patch, + path: path, + params: params, + headers: headers, + cacheModel: cacheModel, + cacheKey: cacheKey, + cacheResponseIdKey: cacheResponseIdKey, + cacheResponseContainerKey: cacheResponseContainerKey, + ); return models.Session.fromMap(res.data); @@ -633,7 +941,21 @@ class Account extends Service { 'content-type': 'application/json', }; - final res = await client.call(HttpMethod.delete, path: path, params: params, headers: headers); + final cacheModel = ''; + final cacheKey = ''; + final cacheResponseIdKey = '\$id'; + final cacheResponseContainerKey = ''; + + final res = await client.call( + HttpMethod.delete, + path: path, + params: params, + headers: headers, + cacheModel: cacheModel, + cacheKey: cacheKey, + cacheResponseIdKey: cacheResponseIdKey, + cacheResponseContainerKey: cacheResponseContainerKey, + ); return res.data; @@ -655,7 +977,21 @@ class Account extends Service { 'content-type': 'application/json', }; - final res = await client.call(HttpMethod.patch, path: path, params: params, headers: headers); + final cacheModel = ''; + final cacheKey = ''; + final cacheResponseIdKey = '\$id'; + final cacheResponseContainerKey = ''; + + final res = await client.call( + HttpMethod.patch, + path: path, + params: params, + headers: headers, + cacheModel: cacheModel, + cacheKey: cacheKey, + cacheResponseIdKey: cacheResponseIdKey, + cacheResponseContainerKey: cacheResponseContainerKey, + ); return models.Account.fromMap(res.data); @@ -690,7 +1026,21 @@ class Account extends Service { 'content-type': 'application/json', }; - final res = await client.call(HttpMethod.post, path: path, params: params, headers: headers); + final cacheModel = ''; + final cacheKey = ''; + final cacheResponseIdKey = '\$id'; + final cacheResponseContainerKey = ''; + + final res = await client.call( + HttpMethod.post, + path: path, + params: params, + headers: headers, + cacheModel: cacheModel, + cacheKey: cacheKey, + cacheResponseIdKey: cacheResponseIdKey, + cacheResponseContainerKey: cacheResponseContainerKey, + ); return models.Token.fromMap(res.data); @@ -715,7 +1065,21 @@ class Account extends Service { 'content-type': 'application/json', }; - final res = await client.call(HttpMethod.put, path: path, params: params, headers: headers); + final cacheModel = ''; + final cacheKey = ''; + final cacheResponseIdKey = '\$id'; + final cacheResponseContainerKey = ''; + + final res = await client.call( + HttpMethod.put, + path: path, + params: params, + headers: headers, + cacheModel: cacheModel, + cacheKey: cacheKey, + cacheResponseIdKey: cacheResponseIdKey, + cacheResponseContainerKey: cacheResponseContainerKey, + ); return models.Token.fromMap(res.data); @@ -740,7 +1104,21 @@ class Account extends Service { 'content-type': 'application/json', }; - final res = await client.call(HttpMethod.post, path: path, params: params, headers: headers); + final cacheModel = ''; + final cacheKey = ''; + final cacheResponseIdKey = '\$id'; + final cacheResponseContainerKey = ''; + + final res = await client.call( + HttpMethod.post, + path: path, + params: params, + headers: headers, + cacheModel: cacheModel, + cacheKey: cacheKey, + cacheResponseIdKey: cacheResponseIdKey, + cacheResponseContainerKey: cacheResponseContainerKey, + ); return models.Token.fromMap(res.data); @@ -765,7 +1143,21 @@ class Account extends Service { 'content-type': 'application/json', }; - final res = await client.call(HttpMethod.put, path: path, params: params, headers: headers); + final cacheModel = ''; + final cacheKey = ''; + final cacheResponseIdKey = '\$id'; + final cacheResponseContainerKey = ''; + + final res = await client.call( + HttpMethod.put, + path: path, + params: params, + headers: headers, + cacheModel: cacheModel, + cacheKey: cacheKey, + cacheResponseIdKey: cacheResponseIdKey, + cacheResponseContainerKey: cacheResponseContainerKey, + ); return models.Token.fromMap(res.data); diff --git a/lib/services/databases.dart b/lib/services/databases.dart index 0608ddb7..d33661a1 100644 --- a/lib/services/databases.dart +++ b/lib/services/databases.dart @@ -21,7 +21,21 @@ class Databases extends Service { 'content-type': 'application/json', }; - final res = await client.call(HttpMethod.get, path: path, params: params, headers: headers); + final cacheModel = '/databases/{databaseId}/collections/{collectionId}/documents'.replaceAll('{databaseId}', databaseId).replaceAll('{collectionId}', collectionId); + final cacheKey = ''; + final cacheResponseIdKey = '\$id'; + final cacheResponseContainerKey = 'documents'; + + final res = await client.call( + HttpMethod.get, + path: path, + params: params, + headers: headers, + cacheModel: cacheModel, + cacheKey: cacheKey, + cacheResponseIdKey: cacheResponseIdKey, + cacheResponseContainerKey: cacheResponseContainerKey, + ); return models.DocumentList.fromMap(res.data); @@ -47,7 +61,21 @@ class Databases extends Service { 'content-type': 'application/json', }; - final res = await client.call(HttpMethod.post, path: path, params: params, headers: headers); + final cacheModel = '/databases/{databaseId}/collections/{collectionId}/documents'.replaceAll('{databaseId}', databaseId).replaceAll('{collectionId}', collectionId); + final cacheKey = documentId; + final cacheResponseIdKey = '\$id'; + final cacheResponseContainerKey = ''; + + final res = await client.call( + HttpMethod.post, + path: path, + params: params, + headers: headers, + cacheModel: cacheModel, + cacheKey: cacheKey, + cacheResponseIdKey: cacheResponseIdKey, + cacheResponseContainerKey: cacheResponseContainerKey, + ); return models.Document.fromMap(res.data); @@ -68,7 +96,21 @@ class Databases extends Service { 'content-type': 'application/json', }; - final res = await client.call(HttpMethod.get, path: path, params: params, headers: headers); + final cacheModel = '/databases/{databaseId}/collections/{collectionId}/documents'.replaceAll('{databaseId}', databaseId).replaceAll('{collectionId}', collectionId).replaceAll('{documentId}', documentId); + final cacheKey = documentId; + final cacheResponseIdKey = '\$id'; + final cacheResponseContainerKey = ''; + + final res = await client.call( + HttpMethod.get, + path: path, + params: params, + headers: headers, + cacheModel: cacheModel, + cacheKey: cacheKey, + cacheResponseIdKey: cacheResponseIdKey, + cacheResponseContainerKey: cacheResponseContainerKey, + ); return models.Document.fromMap(res.data); @@ -91,7 +133,21 @@ class Databases extends Service { 'content-type': 'application/json', }; - final res = await client.call(HttpMethod.patch, path: path, params: params, headers: headers); + final cacheModel = '/databases/{databaseId}/collections/{collectionId}/documents'.replaceAll('{databaseId}', databaseId).replaceAll('{collectionId}', collectionId).replaceAll('{documentId}', documentId); + final cacheKey = documentId; + final cacheResponseIdKey = '\$id'; + final cacheResponseContainerKey = ''; + + final res = await client.call( + HttpMethod.patch, + path: path, + params: params, + headers: headers, + cacheModel: cacheModel, + cacheKey: cacheKey, + cacheResponseIdKey: cacheResponseIdKey, + cacheResponseContainerKey: cacheResponseContainerKey, + ); return models.Document.fromMap(res.data); @@ -111,7 +167,21 @@ class Databases extends Service { 'content-type': 'application/json', }; - final res = await client.call(HttpMethod.delete, path: path, params: params, headers: headers); + final cacheModel = '/databases/{databaseId}/collections/{collectionId}/documents'.replaceAll('{databaseId}', databaseId).replaceAll('{collectionId}', collectionId).replaceAll('{documentId}', documentId); + final cacheKey = documentId; + final cacheResponseIdKey = '\$id'; + final cacheResponseContainerKey = ''; + + final res = await client.call( + HttpMethod.delete, + path: path, + params: params, + headers: headers, + cacheModel: cacheModel, + cacheKey: cacheKey, + cacheResponseIdKey: cacheResponseIdKey, + cacheResponseContainerKey: cacheResponseContainerKey, + ); return res.data; diff --git a/lib/services/functions.dart b/lib/services/functions.dart index 7cb2ee08..1810f1b5 100644 --- a/lib/services/functions.dart +++ b/lib/services/functions.dart @@ -22,7 +22,21 @@ class Functions extends Service { 'content-type': 'application/json', }; - final res = await client.call(HttpMethod.get, path: path, params: params, headers: headers); + final cacheModel = ''; + final cacheKey = ''; + final cacheResponseIdKey = '\$id'; + final cacheResponseContainerKey = 'executions'; + + final res = await client.call( + HttpMethod.get, + path: path, + params: params, + headers: headers, + cacheModel: cacheModel, + cacheKey: cacheKey, + cacheResponseIdKey: cacheResponseIdKey, + cacheResponseContainerKey: cacheResponseContainerKey, + ); return models.ExecutionList.fromMap(res.data); @@ -47,7 +61,21 @@ class Functions extends Service { 'content-type': 'application/json', }; - final res = await client.call(HttpMethod.post, path: path, params: params, headers: headers); + final cacheModel = ''; + final cacheKey = ''; + final cacheResponseIdKey = '\$id'; + final cacheResponseContainerKey = ''; + + final res = await client.call( + HttpMethod.post, + path: path, + params: params, + headers: headers, + cacheModel: cacheModel, + cacheKey: cacheKey, + cacheResponseIdKey: cacheResponseIdKey, + cacheResponseContainerKey: cacheResponseContainerKey, + ); return models.Execution.fromMap(res.data); @@ -67,7 +95,21 @@ class Functions extends Service { 'content-type': 'application/json', }; - final res = await client.call(HttpMethod.get, path: path, params: params, headers: headers); + final cacheModel = ''; + final cacheKey = ''; + final cacheResponseIdKey = '\$id'; + final cacheResponseContainerKey = ''; + + final res = await client.call( + HttpMethod.get, + path: path, + params: params, + headers: headers, + cacheModel: cacheModel, + cacheKey: cacheKey, + cacheResponseIdKey: cacheResponseIdKey, + cacheResponseContainerKey: cacheResponseContainerKey, + ); return models.Execution.fromMap(res.data); diff --git a/lib/services/graphql.dart b/lib/services/graphql.dart index 6e5b698f..2f92eaf1 100644 --- a/lib/services/graphql.dart +++ b/lib/services/graphql.dart @@ -20,7 +20,21 @@ class Graphql extends Service { 'x-sdk-graphql': 'true', 'content-type': 'application/json', }; - final res = await client.call(HttpMethod.post, path: path, params: params, headers: headers); + final cacheModel = ''; + final cacheKey = ''; + final cacheResponseIdKey = '\$id'; + final cacheResponseContainerKey = ''; + + final res = await client.call( + HttpMethod.post, + path: path, + params: params, + headers: headers, + cacheModel: cacheModel, + cacheKey: cacheKey, + cacheResponseIdKey: cacheResponseIdKey, + cacheResponseContainerKey: cacheResponseContainerKey, + ); return res.data; @@ -41,7 +55,21 @@ class Graphql extends Service { 'x-sdk-graphql': 'true', 'content-type': 'application/json', }; - final res = await client.call(HttpMethod.post, path: path, params: params, headers: headers); + final cacheModel = ''; + final cacheKey = ''; + final cacheResponseIdKey = '\$id'; + final cacheResponseContainerKey = ''; + + final res = await client.call( + HttpMethod.post, + path: path, + params: params, + headers: headers, + cacheModel: cacheModel, + cacheKey: cacheKey, + cacheResponseIdKey: cacheResponseIdKey, + cacheResponseContainerKey: cacheResponseContainerKey, + ); return res.data; diff --git a/lib/services/locale.dart b/lib/services/locale.dart index 75e104f1..2bc60905 100644 --- a/lib/services/locale.dart +++ b/lib/services/locale.dart @@ -24,7 +24,21 @@ class Locale extends Service { 'content-type': 'application/json', }; - final res = await client.call(HttpMethod.get, path: path, params: params, headers: headers); + final cacheModel = '/locale'; + final cacheKey = 'current'; + final cacheResponseIdKey = '\$id'; + final cacheResponseContainerKey = ''; + + final res = await client.call( + HttpMethod.get, + path: path, + params: params, + headers: headers, + cacheModel: cacheModel, + cacheKey: cacheKey, + cacheResponseIdKey: cacheResponseIdKey, + cacheResponseContainerKey: cacheResponseContainerKey, + ); return models.Locale.fromMap(res.data); @@ -45,7 +59,21 @@ class Locale extends Service { 'content-type': 'application/json', }; - final res = await client.call(HttpMethod.get, path: path, params: params, headers: headers); + final cacheModel = '/locale/continents'; + final cacheKey = ''; + final cacheResponseIdKey = 'code'; + final cacheResponseContainerKey = 'continents'; + + final res = await client.call( + HttpMethod.get, + path: path, + params: params, + headers: headers, + cacheModel: cacheModel, + cacheKey: cacheKey, + cacheResponseIdKey: cacheResponseIdKey, + cacheResponseContainerKey: cacheResponseContainerKey, + ); return models.ContinentList.fromMap(res.data); @@ -66,7 +94,21 @@ class Locale extends Service { 'content-type': 'application/json', }; - final res = await client.call(HttpMethod.get, path: path, params: params, headers: headers); + final cacheModel = '/locale/countries'; + final cacheKey = ''; + final cacheResponseIdKey = 'code'; + final cacheResponseContainerKey = 'countries'; + + final res = await client.call( + HttpMethod.get, + path: path, + params: params, + headers: headers, + cacheModel: cacheModel, + cacheKey: cacheKey, + cacheResponseIdKey: cacheResponseIdKey, + cacheResponseContainerKey: cacheResponseContainerKey, + ); return models.CountryList.fromMap(res.data); @@ -87,7 +129,21 @@ class Locale extends Service { 'content-type': 'application/json', }; - final res = await client.call(HttpMethod.get, path: path, params: params, headers: headers); + final cacheModel = '/locale/countries/eu'; + final cacheKey = ''; + final cacheResponseIdKey = 'code'; + final cacheResponseContainerKey = 'countries'; + + final res = await client.call( + HttpMethod.get, + path: path, + params: params, + headers: headers, + cacheModel: cacheModel, + cacheKey: cacheKey, + cacheResponseIdKey: cacheResponseIdKey, + cacheResponseContainerKey: cacheResponseContainerKey, + ); return models.CountryList.fromMap(res.data); @@ -108,7 +164,21 @@ class Locale extends Service { 'content-type': 'application/json', }; - final res = await client.call(HttpMethod.get, path: path, params: params, headers: headers); + final cacheModel = '/locale/countries/phones'; + final cacheKey = ''; + final cacheResponseIdKey = 'countryCode'; + final cacheResponseContainerKey = 'phones'; + + final res = await client.call( + HttpMethod.get, + path: path, + params: params, + headers: headers, + cacheModel: cacheModel, + cacheKey: cacheKey, + cacheResponseIdKey: cacheResponseIdKey, + cacheResponseContainerKey: cacheResponseContainerKey, + ); return models.PhoneList.fromMap(res.data); @@ -130,7 +200,21 @@ class Locale extends Service { 'content-type': 'application/json', }; - final res = await client.call(HttpMethod.get, path: path, params: params, headers: headers); + final cacheModel = '/locale/currencies'; + final cacheKey = ''; + final cacheResponseIdKey = 'code'; + final cacheResponseContainerKey = 'currencies'; + + final res = await client.call( + HttpMethod.get, + path: path, + params: params, + headers: headers, + cacheModel: cacheModel, + cacheKey: cacheKey, + cacheResponseIdKey: cacheResponseIdKey, + cacheResponseContainerKey: cacheResponseContainerKey, + ); return models.CurrencyList.fromMap(res.data); @@ -151,7 +235,21 @@ class Locale extends Service { 'content-type': 'application/json', }; - final res = await client.call(HttpMethod.get, path: path, params: params, headers: headers); + final cacheModel = '/locale/languages'; + final cacheKey = ''; + final cacheResponseIdKey = 'code'; + final cacheResponseContainerKey = 'languages'; + + final res = await client.call( + HttpMethod.get, + path: path, + params: params, + headers: headers, + cacheModel: cacheModel, + cacheKey: cacheKey, + cacheResponseIdKey: cacheResponseIdKey, + cacheResponseContainerKey: cacheResponseContainerKey, + ); return models.LanguageList.fromMap(res.data); diff --git a/lib/services/storage.dart b/lib/services/storage.dart index 23d09224..d91754dd 100644 --- a/lib/services/storage.dart +++ b/lib/services/storage.dart @@ -21,7 +21,21 @@ class Storage extends Service { 'content-type': 'application/json', }; - final res = await client.call(HttpMethod.get, path: path, params: params, headers: headers); + final cacheModel = ''; + final cacheKey = ''; + final cacheResponseIdKey = '\$id'; + final cacheResponseContainerKey = 'files'; + + final res = await client.call( + HttpMethod.get, + path: path, + params: params, + headers: headers, + cacheModel: cacheModel, + cacheKey: cacheKey, + cacheResponseIdKey: cacheResponseIdKey, + cacheResponseContainerKey: cacheResponseContainerKey, + ); return models.FileList.fromMap(res.data); @@ -94,7 +108,21 @@ class Storage extends Service { 'content-type': 'application/json', }; - final res = await client.call(HttpMethod.get, path: path, params: params, headers: headers); + final cacheModel = ''; + final cacheKey = ''; + final cacheResponseIdKey = '\$id'; + final cacheResponseContainerKey = ''; + + final res = await client.call( + HttpMethod.get, + path: path, + params: params, + headers: headers, + cacheModel: cacheModel, + cacheKey: cacheKey, + cacheResponseIdKey: cacheResponseIdKey, + cacheResponseContainerKey: cacheResponseContainerKey, + ); return models.File.fromMap(res.data); @@ -116,7 +144,21 @@ class Storage extends Service { 'content-type': 'application/json', }; - final res = await client.call(HttpMethod.put, path: path, params: params, headers: headers); + final cacheModel = ''; + final cacheKey = ''; + final cacheResponseIdKey = '\$id'; + final cacheResponseContainerKey = ''; + + final res = await client.call( + HttpMethod.put, + path: path, + params: params, + headers: headers, + cacheModel: cacheModel, + cacheKey: cacheKey, + cacheResponseIdKey: cacheResponseIdKey, + cacheResponseContainerKey: cacheResponseContainerKey, + ); return models.File.fromMap(res.data); @@ -137,7 +179,21 @@ class Storage extends Service { 'content-type': 'application/json', }; - final res = await client.call(HttpMethod.delete, path: path, params: params, headers: headers); + final cacheModel = ''; + final cacheKey = ''; + final cacheResponseIdKey = '\$id'; + final cacheResponseContainerKey = ''; + + final res = await client.call( + HttpMethod.delete, + path: path, + params: params, + headers: headers, + cacheModel: cacheModel, + cacheKey: cacheKey, + cacheResponseIdKey: cacheResponseIdKey, + cacheResponseContainerKey: cacheResponseContainerKey, + ); return res.data; diff --git a/lib/services/teams.dart b/lib/services/teams.dart index c213e693..9532380a 100644 --- a/lib/services/teams.dart +++ b/lib/services/teams.dart @@ -22,7 +22,21 @@ class Teams extends Service { 'content-type': 'application/json', }; - final res = await client.call(HttpMethod.get, path: path, params: params, headers: headers); + final cacheModel = '/teams'; + final cacheKey = ''; + final cacheResponseIdKey = '\$id'; + final cacheResponseContainerKey = 'teams'; + + final res = await client.call( + HttpMethod.get, + path: path, + params: params, + headers: headers, + cacheModel: cacheModel, + cacheKey: cacheKey, + cacheResponseIdKey: cacheResponseIdKey, + cacheResponseContainerKey: cacheResponseContainerKey, + ); return models.TeamList.fromMap(res.data); @@ -47,7 +61,21 @@ class Teams extends Service { 'content-type': 'application/json', }; - final res = await client.call(HttpMethod.post, path: path, params: params, headers: headers); + final cacheModel = ''; + final cacheKey = ''; + final cacheResponseIdKey = '\$id'; + final cacheResponseContainerKey = ''; + + final res = await client.call( + HttpMethod.post, + path: path, + params: params, + headers: headers, + cacheModel: cacheModel, + cacheKey: cacheKey, + cacheResponseIdKey: cacheResponseIdKey, + cacheResponseContainerKey: cacheResponseContainerKey, + ); return models.Team.fromMap(res.data); @@ -67,7 +95,21 @@ class Teams extends Service { 'content-type': 'application/json', }; - final res = await client.call(HttpMethod.get, path: path, params: params, headers: headers); + final cacheModel = '/teams'.replaceAll('{teamId}', teamId); + final cacheKey = teamId; + final cacheResponseIdKey = '\$id'; + final cacheResponseContainerKey = ''; + + final res = await client.call( + HttpMethod.get, + path: path, + params: params, + headers: headers, + cacheModel: cacheModel, + cacheKey: cacheKey, + cacheResponseIdKey: cacheResponseIdKey, + cacheResponseContainerKey: cacheResponseContainerKey, + ); return models.Team.fromMap(res.data); @@ -89,7 +131,21 @@ class Teams extends Service { 'content-type': 'application/json', }; - final res = await client.call(HttpMethod.put, path: path, params: params, headers: headers); + final cacheModel = '/teams'.replaceAll('{teamId}', teamId); + final cacheKey = teamId; + final cacheResponseIdKey = '\$id'; + final cacheResponseContainerKey = ''; + + final res = await client.call( + HttpMethod.put, + path: path, + params: params, + headers: headers, + cacheModel: cacheModel, + cacheKey: cacheKey, + cacheResponseIdKey: cacheResponseIdKey, + cacheResponseContainerKey: cacheResponseContainerKey, + ); return models.Team.fromMap(res.data); @@ -110,7 +166,21 @@ class Teams extends Service { 'content-type': 'application/json', }; - final res = await client.call(HttpMethod.delete, path: path, params: params, headers: headers); + final cacheModel = ''; + final cacheKey = ''; + final cacheResponseIdKey = '\$id'; + final cacheResponseContainerKey = ''; + + final res = await client.call( + HttpMethod.delete, + path: path, + params: params, + headers: headers, + cacheModel: cacheModel, + cacheKey: cacheKey, + cacheResponseIdKey: cacheResponseIdKey, + cacheResponseContainerKey: cacheResponseContainerKey, + ); return res.data; @@ -133,7 +203,21 @@ class Teams extends Service { 'content-type': 'application/json', }; - final res = await client.call(HttpMethod.get, path: path, params: params, headers: headers); + final cacheModel = '/teams/{teamId}/memberships'.replaceAll('{teamId}', teamId); + final cacheKey = ''; + final cacheResponseIdKey = '\$id'; + final cacheResponseContainerKey = 'memberships'; + + final res = await client.call( + HttpMethod.get, + path: path, + params: params, + headers: headers, + cacheModel: cacheModel, + cacheKey: cacheKey, + cacheResponseIdKey: cacheResponseIdKey, + cacheResponseContainerKey: cacheResponseContainerKey, + ); return models.MembershipList.fromMap(res.data); @@ -171,7 +255,21 @@ class Teams extends Service { 'content-type': 'application/json', }; - final res = await client.call(HttpMethod.post, path: path, params: params, headers: headers); + final cacheModel = ''; + final cacheKey = ''; + final cacheResponseIdKey = '\$id'; + final cacheResponseContainerKey = ''; + + final res = await client.call( + HttpMethod.post, + path: path, + params: params, + headers: headers, + cacheModel: cacheModel, + cacheKey: cacheKey, + cacheResponseIdKey: cacheResponseIdKey, + cacheResponseContainerKey: cacheResponseContainerKey, + ); return models.Membership.fromMap(res.data); @@ -192,7 +290,21 @@ class Teams extends Service { 'content-type': 'application/json', }; - final res = await client.call(HttpMethod.get, path: path, params: params, headers: headers); + final cacheModel = '/teams/{teamId}/memberships'.replaceAll('{teamId}', teamId).replaceAll('{membershipId}', membershipId); + final cacheKey = membershipId; + final cacheResponseIdKey = '\$id'; + final cacheResponseContainerKey = ''; + + final res = await client.call( + HttpMethod.get, + path: path, + params: params, + headers: headers, + cacheModel: cacheModel, + cacheKey: cacheKey, + cacheResponseIdKey: cacheResponseIdKey, + cacheResponseContainerKey: cacheResponseContainerKey, + ); return models.Membership.fromMap(res.data); @@ -215,7 +327,21 @@ class Teams extends Service { 'content-type': 'application/json', }; - final res = await client.call(HttpMethod.patch, path: path, params: params, headers: headers); + final cacheModel = ''; + final cacheKey = ''; + final cacheResponseIdKey = '\$id'; + final cacheResponseContainerKey = ''; + + final res = await client.call( + HttpMethod.patch, + path: path, + params: params, + headers: headers, + cacheModel: cacheModel, + cacheKey: cacheKey, + cacheResponseIdKey: cacheResponseIdKey, + cacheResponseContainerKey: cacheResponseContainerKey, + ); return models.Membership.fromMap(res.data); @@ -237,7 +363,21 @@ class Teams extends Service { 'content-type': 'application/json', }; - final res = await client.call(HttpMethod.delete, path: path, params: params, headers: headers); + final cacheModel = ''; + final cacheKey = ''; + final cacheResponseIdKey = '\$id'; + final cacheResponseContainerKey = ''; + + final res = await client.call( + HttpMethod.delete, + path: path, + params: params, + headers: headers, + cacheModel: cacheModel, + cacheKey: cacheKey, + cacheResponseIdKey: cacheResponseIdKey, + cacheResponseContainerKey: cacheResponseContainerKey, + ); return res.data; @@ -265,7 +405,21 @@ class Teams extends Service { 'content-type': 'application/json', }; - final res = await client.call(HttpMethod.patch, path: path, params: params, headers: headers); + final cacheModel = ''; + final cacheKey = ''; + final cacheResponseIdKey = '\$id'; + final cacheResponseContainerKey = ''; + + final res = await client.call( + HttpMethod.patch, + path: path, + params: params, + headers: headers, + cacheModel: cacheModel, + cacheKey: cacheKey, + cacheResponseIdKey: cacheResponseIdKey, + cacheResponseContainerKey: cacheResponseContainerKey, + ); return models.Membership.fromMap(res.data); diff --git a/lib/src/client.dart b/lib/src/client.dart index 7b662f8e..88cb62d7 100644 --- a/lib/src/client.dart +++ b/lib/src/client.dart @@ -1,12 +1,12 @@ -import 'enums.dart'; import 'client_stub.dart' if (dart.library.html) 'client_browser.dart' if (dart.library.io) 'client_io.dart'; +import 'enums.dart'; import 'response.dart'; import 'upload_progress.dart'; abstract class Client { - static const int CHUNK_SIZE = 5*1024*1024; + static const int CHUNK_SIZE = 5 * 1024 * 1024; late Map config; late String _endPoint; late String? _endPointRealtime; @@ -14,9 +14,10 @@ abstract class Client { String get endPoint => _endPoint; String? get endPointRealtime => _endPointRealtime; - factory Client( - {String endPoint = 'https://HOSTNAME/v1', - bool selfSigned = false}) => + factory Client({ + String endPoint = 'https://HOSTNAME/v1', + bool selfSigned = false, + }) => createClient(endPoint: endPoint, selfSigned: selfSigned); Future webAuth(Uri url, {String? callbackUrlScheme}); @@ -36,18 +37,35 @@ abstract class Client { Client setEndPointRealtime(String endPoint); - /// Your project ID + /// Your project ID Client setProject(value); - /// Your secret JSON Web Token + /// Your secret JSON Web Token Client setJWT(value); Client setLocale(value); Client addHeader(String key, String value); - Future call(HttpMethod method, { + Future call( + HttpMethod method, { String path = '', Map headers = const {}, Map params = const {}, ResponseType? responseType, + String cacheModel = '', + String cacheKey = '', + String cacheResponseIdKey = '', + String cacheResponseContainerKey = '', + Map? previous, + }); + + Future setOfflinePersistency({ + bool status = true, + void Function(Object)? onWriteQueueError, }); + + bool getOfflinePersistency(); + + Client setOfflineCacheSize(int kbytes); + + int getOfflineCacheSize(); } diff --git a/lib/src/client_base.dart b/lib/src/client_base.dart index 4825521e..cdfe8a82 100644 --- a/lib/src/client_base.dart +++ b/lib/src/client_base.dart @@ -1,14 +1,16 @@ -import 'response.dart'; import 'client.dart'; import 'enums.dart'; +import 'response.dart'; -abstract class ClientBase implements Client { - /// Your project ID +abstract class ClientBase implements Client { + /// Your project ID @override ClientBase setProject(value); - /// Your secret JSON Web Token + + /// Your secret JSON Web Token @override ClientBase setJWT(value); + @override ClientBase setLocale(value); @@ -31,5 +33,25 @@ abstract class ClientBase implements Client { Map headers = const {}, Map params = const {}, ResponseType? responseType, + String cacheModel = '', + String cacheKey = '', + String cacheResponseIdKey = '', + String cacheResponseContainerKey = '', + Map? previous, + }); + + @override + Future setOfflinePersistency({ + bool status = true, + void Function(Object)? onWriteQueueError, }); + + @override + bool getOfflinePersistency(); + + @override + ClientBase setOfflineCacheSize(int kbytes); + + @override + int getOfflineCacheSize(); } diff --git a/lib/src/client_browser.dart b/lib/src/client_browser.dart index 709c9b78..79db3dbd 100644 --- a/lib/src/client_browser.dart +++ b/lib/src/client_browser.dart @@ -1,16 +1,20 @@ +import 'dart:io'; import 'dart:math'; + import 'package:flutter/foundation.dart'; import 'package:flutter_web_auth_2/flutter_web_auth_2.dart'; -import 'package:http/http.dart' as http; import 'package:http/browser_client.dart'; +import 'package:http/http.dart' as http; import 'package:universal_html/html.dart' as html; + +import 'client_base.dart'; import 'client_mixin.dart'; +import 'client_offline_mixin.dart'; import 'enums.dart'; import 'exception.dart'; -import 'client_base.dart'; import 'input_file.dart'; -import 'upload_progress.dart'; import 'response.dart'; +import 'upload_progress.dart'; ClientBase createClient({ required String endPoint, @@ -18,14 +22,16 @@ ClientBase createClient({ }) => ClientBrowser(endPoint: endPoint, selfSigned: selfSigned); -class ClientBrowser extends ClientBase with ClientMixin { - static const int CHUNK_SIZE = 5*1024*1024; +class ClientBrowser extends ClientBase with ClientMixin, ClientOfflineMixin { + static const int CHUNK_SIZE = 5 * 1024 * 1024; String _endPoint; Map? _headers; @override late Map config; late BrowserClient _httpClient; String? _endPointRealtime; + bool _offlinePersistency = false; + int _maxCacheSize = 40000; // 40MB @override String? get endPointRealtime => _endPointRealtime; @@ -43,8 +49,8 @@ class ClientBrowser extends ClientBase with ClientMixin { 'x-sdk-name': 'Flutter', 'x-sdk-platform': 'client', 'x-sdk-language': 'flutter', - 'x-sdk-version': '8.3.0', - 'X-Appwrite-Response-Format' : '1.0.0', + 'x-sdk-version': '9.0.0', + 'X-Appwrite-Response-Format': '1.0.0', }; config = {}; @@ -57,26 +63,28 @@ class ClientBrowser extends ClientBase with ClientMixin { @override String get endPoint => _endPoint; - /// Your project ID - @override - ClientBrowser setProject(value) { - config['project'] = value; - addHeader('X-Appwrite-Project', value); - return this; - } - /// Your secret JSON Web Token - @override - ClientBrowser setJWT(value) { - config['jWT'] = value; - addHeader('X-Appwrite-JWT', value); - return this; - } - @override - ClientBrowser setLocale(value) { - config['locale'] = value; - addHeader('X-Appwrite-Locale', value); - return this; - } + /// Your project ID + @override + ClientBrowser setProject(value) { + config['project'] = value; + addHeader('X-Appwrite-Project', value); + return this; + } + + /// Your secret JSON Web Token + @override + ClientBrowser setJWT(value) { + config['jWT'] = value; + addHeader('X-Appwrite-JWT', value); + return this; + } + + @override + ClientBrowser setLocale(value) { + config['locale'] = value; + addHeader('X-Appwrite-Locale', value); + return this; + } @override ClientBrowser setSelfSigned({bool status = true}) { @@ -98,6 +106,38 @@ class ClientBrowser extends ClientBase with ClientMixin { return this; } + bool getOfflinePersistency() { + return _offlinePersistency; + } + + @override + Future setOfflinePersistency( + {bool status = true, void Function(Object)? onWriteQueueError}) async { + _offlinePersistency = status; + + if (_offlinePersistency) { + await initOffline( + call: call, + onWriteQueueError: onWriteQueueError, + getOfflineCacheSize: getOfflineCacheSize, + ); + } + + return this; + } + + @override + ClientBrowser setOfflineCacheSize(int kbytes) { + _maxCacheSize = kbytes * 1000; + + return this; + } + + @override + int getOfflineCacheSize() { + return _maxCacheSize; + } + @override ClientBrowser addHeader(String key, String value) { _headers![key] = value; @@ -131,7 +171,11 @@ class ClientBrowser extends ClientBase with ClientMixin { late Response res; if (size <= CHUNK_SIZE) { - params[paramName] = http.MultipartFile.fromBytes(paramName, file.bytes!, filename: file.filename); + params[paramName] = http.MultipartFile.fromBytes( + paramName, + file.bytes!, + filename: file.filename, + ); return call( HttpMethod.post, path: path, @@ -158,8 +202,11 @@ class ClientBrowser extends ClientBase with ClientMixin { var chunk; final end = min(offset + CHUNK_SIZE, size); chunk = file.bytes!.getRange(offset, end).toList(); - params[paramName] = - http.MultipartFile.fromBytes(paramName, chunk, filename: file.filename); + params[paramName] = http.MultipartFile.fromBytes( + paramName, + chunk, + filename: file.filename, + ); headers['content-range'] = 'bytes $offset-${min(((offset + CHUNK_SIZE) - 1), size)}/$size'; res = await call(HttpMethod.post, @@ -187,6 +234,98 @@ class ClientBrowser extends ClientBase with ClientMixin { Map headers = const {}, Map params = const {}, ResponseType? responseType, + String cacheModel = '', + String cacheKey = '', + String cacheResponseIdKey = '', + String cacheResponseContainerKey = '', + Map? previous, + }) async { + while (true) { + final uri = Uri.parse(endPoint + path); + + http.BaseRequest request = prepareRequest( + method, + uri: uri, + headers: {..._headers!, ...headers}, + params: params, + ); + + if (getOfflinePersistency() && !isOnline.value) { + await checkOnlineStatus(); + } + + if (cacheModel.isNotEmpty && + getOfflinePersistency() && + !isOnline.value && + responseType != ResponseType.bytes) { + return handleOfflineRequest( + uri: uri, + method: method, + call: call, + path: path, + headers: headers, + params: params, + responseType: responseType, + cacheModel: cacheModel, + cacheKey: cacheKey, + cacheResponseIdKey: cacheResponseIdKey, + cacheResponseContainerKey: cacheResponseContainerKey, + ); + } + + try { + final response = await send( + method, + path: path, + headers: headers, + params: params, + responseType: responseType, + cacheModel: cacheModel, + cacheKey: cacheKey, + cacheResponseIdKey: cacheResponseIdKey, + cacheResponseContainerKey: cacheResponseContainerKey, + ); + + if (getOfflinePersistency()) { + cacheResponse( + cacheModel: cacheModel, + cacheKey: cacheKey, + cacheResponseIdKey: cacheResponseIdKey, + request: request, + response: response, + ); + } + + return response; + } on AppwriteException catch (e) { + if ((e.message != "Network is unreachable" && + !(e.message?.contains("Failed host lookup") ?? false)) || + !getOfflinePersistency()) { + rethrow; + } + isOnline.value = false; + } on SocketException catch (_) { + if (!getOfflinePersistency()) { + rethrow; + } + isOnline.value = false; + } catch (e) { + throw AppwriteException(e.toString()); + } + } + } + + Future send( + HttpMethod method, { + String path = '', + Map headers = const {}, + Map params = const {}, + ResponseType? responseType, + String cacheModel = '', + String cacheKey = '', + String cacheResponseIdKey = '', + String cacheResponseContainerKey = '', + Map? previous, }) async { await init(); @@ -219,7 +358,7 @@ class ClientBrowser extends ClientBase with ClientMixin { @override Future webAuth(Uri url, {String? callbackUrlScheme}) { - return FlutterWebAuth2.authenticate( + return FlutterWebAuth2.authenticate( url: url.toString(), callbackUrlScheme: "appwrite-callback-" + config['project']!, ); diff --git a/lib/src/client_io.dart b/lib/src/client_io.dart index 7c45b2af..3b29c760 100644 --- a/lib/src/client_io.dart +++ b/lib/src/client_io.dart @@ -1,21 +1,25 @@ +import 'dart:async'; import 'dart:io'; import 'dart:math'; + import 'package:cookie_jar/cookie_jar.dart'; import 'package:device_info_plus/device_info_plus.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_web_auth_2/flutter_web_auth_2.dart'; import 'package:http/http.dart' as http; import 'package:http/io_client.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:path_provider/path_provider.dart'; -import 'package:flutter_web_auth_2/flutter_web_auth_2.dart'; -import 'client_mixin.dart'; + import 'client_base.dart'; +import 'client_mixin.dart'; +import 'client_offline_mixin.dart'; import 'cookie_manager.dart'; import 'enums.dart'; import 'exception.dart'; +import 'input_file.dart'; import 'interceptor.dart'; import 'response.dart'; -import 'package:flutter/foundation.dart'; -import 'input_file.dart'; import 'upload_progress.dart'; ClientBase createClient({ @@ -27,8 +31,8 @@ ClientBase createClient({ selfSigned: selfSigned, ); -class ClientIO extends ClientBase with ClientMixin { - static const int CHUNK_SIZE = 5*1024*1024; +class ClientIO extends ClientBase with ClientMixin, ClientOfflineMixin { + static const int CHUNK_SIZE = 5 * 1024 * 1024; String _endPoint; Map? _headers; @override @@ -47,6 +51,8 @@ class ClientIO extends ClientBase with ClientMixin { CookieJar get cookieJar => _cookieJar; @override String? get endPointRealtime => _endPointRealtime; + bool _offlinePersistency = false; + int _maxCacheSize = 40000; // 40MB ClientIO({ String endPoint = 'https://HOSTNAME/v1', @@ -64,8 +70,8 @@ class ClientIO extends ClientBase with ClientMixin { 'x-sdk-name': 'Flutter', 'x-sdk-platform': 'client', 'x-sdk-language': 'flutter', - 'x-sdk-version': '8.3.0', - 'X-Appwrite-Response-Format' : '1.0.0', + 'x-sdk-version': '9.0.0', + 'X-Appwrite-Response-Format': '1.0.0', }; config = {}; @@ -86,26 +92,28 @@ class ClientIO extends ClientBase with ClientMixin { return dir; } - /// Your project ID - @override - ClientIO setProject(value) { - config['project'] = value; - addHeader('X-Appwrite-Project', value); - return this; - } - /// Your secret JSON Web Token - @override - ClientIO setJWT(value) { - config['jWT'] = value; - addHeader('X-Appwrite-JWT', value); - return this; - } - @override - ClientIO setLocale(value) { - config['locale'] = value; - addHeader('X-Appwrite-Locale', value); - return this; - } + /// Your project ID + @override + ClientIO setProject(value) { + config['project'] = value; + addHeader('X-Appwrite-Project', value); + return this; + } + + /// Your secret JSON Web Token + @override + ClientIO setJWT(value) { + config['jWT'] = value; + addHeader('X-Appwrite-JWT', value); + return this; + } + + @override + ClientIO setLocale(value) { + config['locale'] = value; + addHeader('X-Appwrite-Locale', value); + return this; + } @override ClientIO setSelfSigned({bool status = true}) { @@ -130,6 +138,38 @@ class ClientIO extends ClientBase with ClientMixin { return this; } + bool getOfflinePersistency() { + return _offlinePersistency; + } + + @override + Future setOfflinePersistency( + {bool status = true, void Function(Object)? onWriteQueueError}) async { + _offlinePersistency = status; + + if (_offlinePersistency) { + await initOffline( + call: call, + onWriteQueueError: onWriteQueueError, + getOfflineCacheSize: getOfflineCacheSize, + ); + } + + return this; + } + + @override + ClientIO setOfflineCacheSize(int kbytes) { + _maxCacheSize = kbytes * 1000; + + return this; + } + + @override + int getOfflineCacheSize() { + return _maxCacheSize; + } + @override ClientIO addHeader(String key, String value) { _headers![key] = value; @@ -138,7 +178,7 @@ class ClientIO extends ClientBase with ClientMixin { } Future init() async { - if(_initProgress) return; + if (_initProgress) return; _initProgress = true; final Directory cookieDir = await _getCookiePath(); _cookieJar = PersistCookieJar(storage: FileStorage(cookieDir.path)); @@ -147,8 +187,10 @@ class ClientIO extends ClientBase with ClientMixin { var device = ''; try { PackageInfo packageInfo = await PackageInfo.fromPlatform(); - addHeader('Origin', - 'appwrite-${Platform.operatingSystem}://${packageInfo.packageName}'); + addHeader( + 'Origin', + 'appwrite-${Platform.operatingSystem}://${packageInfo.packageName}', + ); //creating custom user agent DeviceInfoPlugin deviceInfoPlugin = DeviceInfoPlugin(); @@ -175,12 +217,13 @@ class ClientIO extends ClientBase with ClientMixin { device = '(Macintosh; ${macinfo.model})'; } addHeader( - 'user-agent', '${packageInfo.packageName}/${packageInfo.version} $device'); + 'user-agent', + '${packageInfo.packageName}/${packageInfo.version} $device', + ); } catch (e) { debugPrint('Error getting device info: $e'); device = Platform.operatingSystem; - addHeader( - 'user-agent', '$device'); + addHeader('user-agent', '$device'); } _initialized = true; @@ -246,11 +289,16 @@ class ClientIO extends ClientBase with ClientMixin { if (size <= CHUNK_SIZE) { if (file.path != null) { params[paramName] = await http.MultipartFile.fromPath( - paramName, file.path!, - filename: file.filename); + paramName, + file.path!, + filename: file.filename, + ); } else { - params[paramName] = http.MultipartFile.fromBytes(paramName, file.bytes!, - filename: file.filename); + params[paramName] = http.MultipartFile.fromBytes( + paramName, + file.bytes!, + filename: file.filename, + ); } return call( HttpMethod.post, @@ -281,20 +329,27 @@ class ClientIO extends ClientBase with ClientMixin { } while (offset < size) { - var chunk; + List chunk = []; if (file.bytes != null) { - final end = min(offset + CHUNK_SIZE-1, size-1); + final end = min(offset + CHUNK_SIZE - 1, size - 1); chunk = file.bytes!.getRange(offset, end).toList(); } else { raf!.setPositionSync(offset); chunk = raf.readSync(CHUNK_SIZE); } - params[paramName] = - http.MultipartFile.fromBytes(paramName, chunk, filename: file.filename); + params[paramName] = http.MultipartFile.fromBytes( + paramName, + chunk, + filename: file.filename, + ); headers['content-range'] = 'bytes $offset-${min(((offset + CHUNK_SIZE) - 1), size)}/$size'; - res = await call(HttpMethod.post, - path: path, headers: headers, params: params); + res = await call( + HttpMethod.post, + path: path, + headers: headers, + params: params, + ); offset += CHUNK_SIZE; if (offset < size) { headers['x-appwrite-id'] = res.data['\$id']; @@ -316,7 +371,9 @@ class ClientIO extends ClientBase with ClientMixin { Future webAuth(Uri url, {String? callbackUrlScheme}) { return FlutterWebAuth2.authenticate( url: url.toString(), - callbackUrlScheme: callbackUrlScheme != null && Platform.isWindows ? callbackUrlScheme : "appwrite-callback-" + config['project']!, + callbackUrlScheme: callbackUrlScheme != null && Platform.isWindows + ? callbackUrlScheme + : "appwrite-callback-" + config['project']!, preferEphemeral: true, ).then((value) async { Uri url = Uri.parse(value); @@ -336,13 +393,17 @@ class ClientIO extends ClientBase with ClientMixin { }); } - @override - Future call( + Future send( HttpMethod method, { String path = '', Map headers = const {}, Map params = const {}, ResponseType? responseType, + String cacheModel = '', + String cacheKey = '', + String cacheResponseIdKey = '', + String cacheResponseContainerKey = '', + Map? previous, }) async { while (!_initialized && _initProgress) { await Future.delayed(Duration(milliseconds: 10)); @@ -351,10 +412,10 @@ class ClientIO extends ClientBase with ClientMixin { await init(); } - late http.Response res; + final uri = Uri.parse(_endPoint + path); http.BaseRequest request = prepareRequest( method, - uri: Uri.parse(_endPoint + path), + uri: uri, headers: {..._headers!, ...headers}, params: params, ); @@ -362,13 +423,15 @@ class ClientIO extends ClientBase with ClientMixin { try { request = await _interceptRequest(request); final streamedResponse = await _httpClient.send(request); - res = await toResponse(streamedResponse); + http.Response res = await toResponse(streamedResponse); res = await _interceptResponse(res); - return prepareResponse( + final response = prepareResponse( res, responseType: responseType, ); + + return response; } catch (e) { if (e is AppwriteException) { rethrow; @@ -376,4 +439,92 @@ class ClientIO extends ClientBase with ClientMixin { throw AppwriteException(e.toString()); } } + + @override + Future call( + HttpMethod method, { + String path = '', + Map headers = const {}, + Map params = const {}, + ResponseType? responseType, + String cacheModel = '', + String cacheKey = '', + String cacheResponseIdKey = '', + String cacheResponseContainerKey = '', + Map? previous, + }) async { + while (true) { + final uri = Uri.parse(endPoint + path); + + http.BaseRequest request = prepareRequest( + method, + uri: uri, + headers: {..._headers!, ...headers}, + params: params, + ); + + if (getOfflinePersistency() && !isOnline.value) { + await checkOnlineStatus(); + } + + if (cacheModel.isNotEmpty && + getOfflinePersistency() && + !isOnline.value && + responseType != ResponseType.bytes) { + return handleOfflineRequest( + uri: uri, + method: method, + call: call, + path: path, + headers: headers, + params: params, + responseType: responseType, + cacheModel: cacheModel, + cacheKey: cacheKey, + cacheResponseIdKey: cacheResponseIdKey, + cacheResponseContainerKey: cacheResponseContainerKey, + ); + } + + try { + final response = await send( + method, + path: path, + headers: headers, + params: params, + responseType: responseType, + cacheModel: cacheModel, + cacheKey: cacheKey, + cacheResponseIdKey: cacheResponseIdKey, + cacheResponseContainerKey: cacheResponseContainerKey, + ); + + if (getOfflinePersistency()) { + cacheResponse( + cacheModel: cacheModel, + cacheKey: cacheKey, + cacheResponseIdKey: cacheResponseIdKey, + request: request, + response: response, + ); + } + + return response; + } on AppwriteException catch (e) { + if ((e.message != "Network is unreachable" && + !(e.message?.contains("Failed host lookup") ?? false)) || + !getOfflinePersistency()) { + rethrow; + } + isOnline.value = false; + } on SocketException catch (_) { + if (!getOfflinePersistency()) { + rethrow; + } + isOnline.value = false; + } catch (e) { + throw AppwriteException(e.toString()); + } + } + } } diff --git a/lib/src/client_offline_mixin.dart b/lib/src/client_offline_mixin.dart new file mode 100644 index 00000000..ad9bcac1 --- /dev/null +++ b/lib/src/client_offline_mixin.dart @@ -0,0 +1,662 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:flutter/foundation.dart'; +import 'package:http/http.dart' as http; +import 'package:sembast/sembast.dart'; +import 'package:sembast/timestamp.dart'; +import 'package:sembast/utils/value_utils.dart'; + +import 'enums.dart'; +import 'exception.dart'; +import 'offline_db_stub.dart' + if (dart.library.html) 'offline_db_web.dart' + if (dart.library.io) 'offline_db_io.dart'; +import 'response.dart'; + +class AccessTimestamp { + final String model; + final String key; + final Timestamp accessedAt; + + AccessTimestamp({ + required this.model, + required this.key, + required this.accessedAt, + }); + + factory AccessTimestamp.fromMap(Map json) => AccessTimestamp( + model: json["model"] as String, + key: json["key"] as String, + accessedAt: json["accessedAt"] as Timestamp, + ); + + Map toMap() => { + "model": model, + "key": key, + "accessedAt": accessedAt, + }; +} + +class ClientOfflineMixin { + static const defaultLimit = 25; + ValueNotifier isOnline = ValueNotifier(true); + late Database db; + + StoreRef> _queuedWritesStore = + stringMapStoreFactory.store('queuedWrites'); + StoreRef> _accessTimestampsStore = + stringMapStoreFactory.store('accessTimestamps'); + StoreRef _cacheSizeStore = StoreRef('cacheSize'); + + Future initOffline({ + required Future> Function( + HttpMethod, { + String path, + Map headers, + Map params, + ResponseType? responseType, + String cacheModel, + String cacheKey, + String cacheResponseIdKey, + String cacheResponseContainerKey, + }) + call, + void Function(Object)? onWriteQueueError, + required int Function() getOfflineCacheSize, + }) async { + await Future.wait([initOfflineDatabase(), listenForConnectivity()]); + await processWriteQueue(call, onError: onWriteQueueError); + + final cacheSizeRecordRef = getCacheSizeRecordRef(); + cacheSizeRecordRef.onSnapshot(db).listen((snapshot) { + int? currentSize = snapshot?.value; + + if (currentSize == null || currentSize < getOfflineCacheSize()) return; + + db.transaction((txn) async { + final records = await listAccessedAt(txn); + if (records.isEmpty) return; + final record = records.first; + final modelStore = getModelStore(record.value['model'] as String); + final cacheKey = record.value['key'] as String; + await deleteCache(txn, modelStore, key: cacheKey); + }); + }); + } + + Future initOfflineDatabase() async { + db = await OfflineDatabase.instance.db(); + } + + Future processWriteQueue( + Future> Function( + HttpMethod, { + String path, + Map headers, + Map params, + ResponseType? responseType, + String cacheModel, + String cacheKey, + String cacheResponseIdKey, + String cacheResponseContainerKey, + }) + call, + {void Function(Object e)? onError}) async { + if (!isOnline.value) return; + final queuedWriteRecords = await listQueuedWrites(db); + for (final queuedWriteRecord in queuedWriteRecords) { + final queuedWrite = queuedWriteRecord.value; + try { + final method = HttpMethod.values + .where((v) => v.name() == queuedWrite['method']) + .first; + final path = queuedWrite['path'] as String; + final headers = (queuedWrite['headers'] as Map) + .map((key, value) => MapEntry(key, value?.toString() ?? '')); + final params = queuedWrite['params'] as Map; + final cacheModel = queuedWrite['cacheModel'] as String; + final cacheKey = queuedWrite['cacheKey'] as String; + final cacheResponseContainerKey = + queuedWrite['cacheResponseContainerKey'] as String; + final cacheResponseIdKey = queuedWrite['cacheResponseIdKey'] as String; + final res = await call( + method, + path: path, + headers: headers, + params: params, + cacheModel: cacheModel, + cacheKey: cacheKey, + cacheResponseContainerKey: cacheResponseContainerKey, + cacheResponseIdKey: cacheResponseIdKey, + ); + + final modelStore = getModelStore(cacheModel); + db.transaction((txn) async { + final futures = []; + if (method == HttpMethod.post) { + final recordKey = res.data['\$id']; + futures.add( + upsertCache( + txn, + modelStore, + res.data, + key: recordKey, + ), + ); + } + + futures.add(queuedWriteRecord.ref.delete(txn)); + + await Future.wait(futures); + }); + } on AppwriteException catch (e) { + if (onError != null) { + onError(e); + } + if ((e.code ?? 0) >= 400) { + db.transaction((txn) async { + final queuedWriteKey = queuedWriteRecord.key; + await deleteQueuedWrite(txn, queuedWriteKey); + // restore cache + final previous = queuedWrite['previous'] as Map?; + final cacheModel = queuedWrite['cacheModel'] as String; + final cacheKey = queuedWrite['cacheKey'] as String; + final modelStore = getModelStore(cacheModel); + if (previous != null) { + await upsertCache(txn, modelStore, previous, key: cacheKey); + } + }); + } + } catch (e) { + if (onError != null) { + onError(e); + } + } + } + } + + bool resultIsOnline(ConnectivityResult result) { + return result == ConnectivityResult.mobile || + result == ConnectivityResult.ethernet || + result == ConnectivityResult.wifi; + } + + Future checkOnlineStatus() async { + try { + final url = Uri.parse('https://appwrite.io/version'); + await http.head(url).timeout(Duration(seconds: 1)); + isOnline.value = true; + } catch (_) { + isOnline.value = false; + } + } + + Future listenForConnectivity() async { + void handleConnectivityResult(ConnectivityResult result) { + isOnline.value = resultIsOnline(result); + } + + final result = await Connectivity().checkConnectivity(); + handleConnectivityResult(result); + if (isOnline.value) { + // wifi or mobile is connected, but double check internet connectivity + await checkOnlineStatus(); + } + Connectivity().onConnectivityChanged.listen(handleConnectivityResult); + } + + Future handleOfflineRequest({ + required Uri uri, + required HttpMethod method, + required Future> Function(HttpMethod, + {String path, + Map headers, + Map params, + ResponseType? responseType, + String cacheModel, + String cacheKey, + String cacheResponseIdKey, + String cacheResponseContainerKey}) + call, + String path = '', + Map headers = const {}, + Map params = const {}, + ResponseType? responseType, + String cacheModel = '', + String cacheKey = '', + String cacheResponseIdKey = '', + String cacheResponseContainerKey = '', + Map? previous, + }) async { + if (!headers.containsKey('X-Appwrite-Timestamp')) { + headers['X-Appwrite-Timestamp'] = + DateTime.now().toUtc().toIso8601String(); + } + final pathSegments = uri.pathSegments; + String queuedWriteKey = ''; + + final store = getModelStore(cacheModel); + if (method == HttpMethod.get) { + if (cacheKey.isNotEmpty) { + final recordRef = store.record(cacheKey); + final record = await recordRef.get(db); + if (record == null) { + throw AppwriteException( + "Client is offline and data is not cached", + 0, + "general_offline", + ); + } + updateAccessedAt(db, store.name, cacheKey); + return Response(data: record); + } else { + final finder = Finder(limit: defaultLimit); + // TODO: await both at same time + final records = await store.find(db, finder: finder); + db.transaction((txn) async { + for (final record in records) { + await updateAccessedAt(txn, store.name, record.key); + } + }); + final count = await store.count(db); + return Response(data: { + 'total': count, + cacheResponseContainerKey: records.map((record) { + final map = Map(); + record.value.entries.forEach((entry) { + map[entry.key] = entry.value; + }); + return map; + }).toList(), + }); + } + } + switch (method) { + case HttpMethod.get: + // already handled + break; + case HttpMethod.post: + if (params.containsKey('data')) { + final documentId = params['documentId']; + cacheKey = documentId; + final document = Map.from(params['data']); + final now = DateTime.now().toUtc().toIso8601String(); + document['\$createdAt'] = now; + document['\$updatedAt'] = now; + document['\$id'] = documentId; + document['\$collectionId'] = pathSegments[4]; + document['\$databaseId'] = pathSegments[2]; + document['\$permissions'] = params['permissions']; + await db.transaction((txn) async { + await upsertCache(txn, store, document, key: cacheKey); + queuedWriteKey = await addQueuedWrite( + txn, + method, + path, + headers, + params, + cacheModel, + cacheKey, + cacheResponseIdKey, + cacheResponseContainerKey, + null, + ); + }); + } + break; + case HttpMethod.delete: + if (cacheKey.isNotEmpty) { + await db.transaction((txn) async { + final previous = await store.record(cacheKey).get(txn); + await deleteCache(txn, store, key: cacheKey); + queuedWriteKey = await addQueuedWrite( + txn, + method, + path, + headers, + params, + cacheModel, + cacheKey, + cacheResponseIdKey, + cacheResponseContainerKey, + previous, + ); + }); + } + break; + case HttpMethod.put: + case HttpMethod.patch: + final entry = Map(); + if (params.containsKey('data')) { + entry.addAll(Map.from(params['data'])); + final now = DateTime.now().toUtc().toIso8601String(); + entry['\$createdAt'] = now; + entry['\$updatedAt'] = now; + entry['\$id'] = cacheKey; + } else if (params.containsKey('prefs')) { + entry.addAll(Map.from(params['prefs'])); + } + + await db.transaction((txn) async { + final previous = await store.record(cacheKey).get(txn); + if (previous != null && previous.containsKey('\$createdAt')) { + entry['\$createdAt'] = previous['\$createdAt']; + } + await upsertCache(txn, store, entry, key: cacheKey); + queuedWriteKey = await addQueuedWrite( + txn, + method, + path, + headers, + params, + cacheModel, + cacheKey, + cacheResponseIdKey, + cacheResponseContainerKey, + previous, + ); + }); + break; + } + final completer = Completer(); + // Declare listener first so it can be referenced inside itself + Function() listener = () {}; + listener = () async { + while (true) { + final queuedWrites = await listQueuedWrites(db); + + if (queuedWrites.isEmpty) { + break; + } + + if (queuedWrites.first.key != queuedWriteKey) { + await Future.delayed(Duration.zero); + continue; + } + + try { + final res = await call( + method, + headers: headers, + params: params, + path: path, + responseType: responseType, + ); + + await db.transaction((txn) async { + final futures = []; + if (method == HttpMethod.post) { + futures.add(upsertCache(txn, store, res.data, key: cacheKey)); + } + + futures.add(deleteQueuedWrite(txn, queuedWriteKey)); + + await Future.wait(futures); + }); + + completer.complete(res); + } on AppwriteException catch (e) { + if (e.message == "Bad state: Can't finalize a finalized Request.") { + continue; + } + if (!completer.isCompleted) { + if (e.code == 404) { + // delete from cache + await db.transaction((txn) async { + await deleteCache(txn, store, key: cacheKey); + await deleteQueuedWrite(txn, queuedWriteKey); + }); + } else if ((e.code ?? 0) >= 400) { + // restore cache + final previous = + queuedWrites.first.value['previous'] as Map?; + await db.transaction((txn) async { + if (previous != null) { + await upsertCache(txn, store, previous, key: cacheKey); + } + await deleteQueuedWrite(txn, queuedWriteKey); + }); + } + completer.completeError(e); + } + } catch (e) { + if (!completer.isCompleted) { + // restore cache + final previous = + queuedWrites.first.value['previous'] as Map?; + if (previous != null) { + await db.transaction((txn) async { + await upsertCache(txn, store, previous, key: cacheKey); + await deleteQueuedWrite(txn, queuedWriteKey); + }); + } + completer.completeError(e); + } + } + isOnline.removeListener(listener); + break; + } + }; + isOnline.addListener(listener); + return completer.future; + } + + void cacheResponse({ + required String cacheModel, + required String cacheKey, + required String cacheResponseIdKey, + required http.BaseRequest request, + required Response response, + }) { + if (cacheModel.isEmpty) return; + + final store = getModelStore(cacheModel); + switch (request.method) { + case 'GET': + final clone = cloneMap(response.data); + if (cacheKey.isNotEmpty) { + db.transaction((txn) async { + await upsertCache(txn, store, clone, key: cacheKey); + }); + } else { + clone.forEach((key, value) { + if (key == 'total') return; + db.transaction((txn) async { + for (final element in value as List) { + final map = element as Map; + final id = map[cacheResponseIdKey]; + await upsertCache(txn, store, map, key: id); + } + }); + }); + } + break; + case 'POST': + case 'PUT': + case 'PATCH': + Map clone = cloneMap(response.data); + if (cacheKey.isEmpty) { + cacheKey = clone['\$id'] as String; + } + if (cacheModel.endsWith('/prefs')) { + clone = response.data['prefs']; + } + db.transaction((txn) async { + await upsertCache(txn, store, clone, key: cacheKey); + }); + break; + case 'DELETE': + if (cacheKey.isNotEmpty) { + db.transaction((txn) async { + await deleteCache(txn, store, key: cacheKey); + }); + } + } + } + + String encode(Map map) { + final encoded = + jsonEncode(sembastCodecDefault.jsonEncodableCodec.encode(map)); + return encoded; + } + + StoreRef> getModelStore(String model) { + return stringMapStoreFactory.store(model); + } + + Future> upsertCache(DatabaseClient db, + StoreRef> store, Map map, + {String? key, String? id}) async { + if (key == null && id == null) { + throw AppwriteException( + 'key and id cannot be null', 0, 'general_cache_error'); + } + + if (key != null) { + final recordRef = store.record(key); + final record = await recordRef.get(db); + int change = 0; + if (record == null) { + final encoded = encode(map); + change = encoded.length; + } else { + change = calculateChange(record, map); + } + + await updateCacheSize(db, change); + final result = await recordRef.put(db, map, merge: true); + await updateAccessedAt(db, store.name, key); + return result; + } + + final record = await store.findFirst(db, + finder: Finder(filter: Filter.equals('\$id', id))); + + if (record == null) { + final encoded = encode(map); + final change = encoded.length; + await updateCacheSize(db, change); + final key = await store.add(db, map); + await updateAccessedAt(db, store.name, key); + return record!.value; + } + + final updated = await record.ref.put(db, map, merge: true); + final change = calculateChange(record.value, map); + await updateCacheSize(db, change); + return updated; + } + + Future deleteCache( + DatabaseClient db, StoreRef> store, + {String? key, String? id}) async { + if (key == null && id == null) { + throw AppwriteException( + 'key and id cannot be null', + 0, + 'general_cache_error', + ); + } + + RecordSnapshot>? record; + if (key != null) { + record = await store.record(key).getSnapshot(db); + } else { + record = await store.findFirst( + db, + finder: Finder(filter: Filter.equals('\$id', id)), + ); + } + + if (record == null) { + return; + } + final encoded = encode(record.value); + final size = encoded.length; + await updateCacheSize(db, size * -1); + await record.ref.delete(db); + await deleteAccessedAt(db, store.name, record.key); + } + + Future>>> listAccessedAt( + DatabaseClient db) { + final finder = Finder(sortOrders: [SortOrder('accessedAt')]); + return _accessTimestampsStore.find(db, finder: finder); + } + + Future updateAccessedAt( + DatabaseClient db, + String model, + String key, + ) async { + final value = AccessTimestamp( + model: model, + key: key, + accessedAt: Timestamp.now(), + ); + await _accessTimestampsStore.record('$model-$key').put(db, value.toMap()); + } + + Future deleteAccessedAt(DatabaseClient db, String model, String key) { + return _accessTimestampsStore.record('$model-$key').delete(db); + } + + int calculateChange(Map oldMap, Map newMap) { + final oldEncoded = encode(oldMap); + final oldSize = oldEncoded.length; + final newEncoded = encode(newMap); + final newSize = newEncoded.length; + final change = newSize - oldSize; + return change; + } + + RecordRef getCacheSizeRecordRef() { + return _cacheSizeStore.record('cacheSize'); + } + + Future updateCacheSize(DatabaseClient db, int change) async { + if (change == 0) return; + + final record = getCacheSizeRecordRef(); + + final currentSize = await record.get(db) ?? 0; + await record.put(db, currentSize + change); + } + + Future>>> listQueuedWrites( + DatabaseClient db) { + return _queuedWritesStore.find(db); + } + + Future addQueuedWrite( + DatabaseClient db, + HttpMethod method, + String path, + Map headers, + Map params, + String cacheModel, + String cacheKey, + String cacheResponseIdKey, + String cacheResponseContainerKey, + Map? previous, + ) async { + return _queuedWritesStore.add(db, { + 'queuedAt': Timestamp.now(), + 'method': method.name(), + 'path': path, + 'headers': headers, + 'params': params, + 'cacheModel': cacheModel, + 'cacheKey': cacheKey, + 'cacheResponseIdKey': cacheResponseIdKey, + 'cacheResponseContainerKey': cacheResponseContainerKey, + 'previous': previous, + }); + } + + Future deleteQueuedWrite(DatabaseClient db, String key) { + return _queuedWritesStore.record(key).delete(db); + } +} diff --git a/lib/src/offline_db_io.dart b/lib/src/offline_db_io.dart new file mode 100644 index 00000000..61b36422 --- /dev/null +++ b/lib/src/offline_db_io.dart @@ -0,0 +1,25 @@ +import 'dart:io'; + +import 'package:sembast/sembast.dart'; +import 'package:sembast_sqflite/sembast_sqflite.dart'; +import 'package:sqflite/sqflite.dart' as sqflite; +import 'package:sqflite_common_ffi/sqflite_ffi.dart'; + +class OfflineDatabase { + static final OfflineDatabase instance = OfflineDatabase._internal(); + Database? _db; + + OfflineDatabase._internal(); + + Future db() async { + if (_db == null) { + final factory = getDatabaseFactorySqflite( + Platform.isLinux || Platform.isWindows + ? databaseFactoryFfi + : sqflite.databaseFactory, + ); + _db = await factory.openDatabase('appwrite.db'); + } + return _db!; + } +} diff --git a/lib/src/offline_db_stub.dart b/lib/src/offline_db_stub.dart new file mode 100644 index 00000000..b0cf2cb7 --- /dev/null +++ b/lib/src/offline_db_stub.dart @@ -0,0 +1,12 @@ +import 'package:sembast/sembast.dart'; + +class OfflineDatabase { + static final OfflineDatabase instance = OfflineDatabase._internal(); + Database? _db; + + OfflineDatabase._internal(); + + Future db() async { + throw UnimplementedError(); + } +} diff --git a/lib/src/offline_db_web.dart b/lib/src/offline_db_web.dart new file mode 100644 index 00000000..80635614 --- /dev/null +++ b/lib/src/offline_db_web.dart @@ -0,0 +1,17 @@ +import 'package:sembast/sembast.dart'; +import 'package:sembast_web/sembast_web.dart'; + +class OfflineDatabase { + static final OfflineDatabase instance = OfflineDatabase._internal(); + Database? _db; + + OfflineDatabase._internal(); + + Future db() async { + if (_db == null) { + final factory = databaseFactoryWeb; + _db = await factory.openDatabase('appwrite.db'); + } + return _db!; + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 98a67021..6d4158fc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,12 +1,12 @@ name: appwrite -version: 8.3.0 +version: 9.0.0 description: Appwrite is an open-source self-hosted backend server that abstract and simplify complex and repetitive development tasks behind a very simple REST API homepage: https://appwrite.io repository: https://github.com/appwrite/sdk-for-flutter issue_tracker: https://github.com/appwrite/sdk-generator/issues documentation: https://appwrite.io/support environment: - sdk: '>=2.17.0 <3.0.0' + sdk: ">=2.17.0 <3.0.0" dependencies: flutter: @@ -19,6 +19,13 @@ dependencies: path_provider: ^2.0.13 web_socket_channel: ^2.3.0 universal_html: ^2.0.9 + connectivity_plus: ^2.3.9 + path: ^1.8.2 + sembast: ^3.4.0+6 + sembast_sqflite: ^2.1.0+1 + sqflite_common_ffi: ^2.2.2 + sembast_web: ^2.1.0+4 + sqflite: ^2.2.2 dev_dependencies: path_provider_platform_interface: ^2.0.5