diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c723b80..5ab7d326 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Change Log +## 20.3.0 + +* Add `total` parameter to list queries allowing skipping counting rows in a table for improved performance +* Add `Operator` class for atomic modification of rows via update, bulk update, upsert, and bulk upsert operations + ## 20.2.2 * Widen `device_info_plus` and `package_info_plus` dependencies to allow for newer versions for Android 15+ support diff --git a/README.md b/README.md index 34641a20..1653f064 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Add this to your package's `pubspec.yaml` file: ```yml dependencies: - appwrite: ^20.2.2 + appwrite: ^20.3.0 ``` You can install packages from the command line: diff --git a/docs/examples/account/list-identities.md b/docs/examples/account/list-identities.md index 9d2ad83c..31f30b41 100644 --- a/docs/examples/account/list-identities.md +++ b/docs/examples/account/list-identities.md @@ -8,4 +8,5 @@ Account account = Account(client); IdentityList result = await account.listIdentities( queries: [], // optional + total: false, // optional ); diff --git a/docs/examples/account/list-logs.md b/docs/examples/account/list-logs.md index 6d9b1209..a7bb5214 100644 --- a/docs/examples/account/list-logs.md +++ b/docs/examples/account/list-logs.md @@ -8,4 +8,5 @@ Account account = Account(client); LogList result = await account.listLogs( queries: [], // optional + total: false, // optional ); diff --git a/docs/examples/databases/list-documents.md b/docs/examples/databases/list-documents.md index b53120cb..0527c752 100644 --- a/docs/examples/databases/list-documents.md +++ b/docs/examples/databases/list-documents.md @@ -11,4 +11,5 @@ DocumentList result = await databases.listDocuments( collectionId: '', queries: [], // optional transactionId: '', // optional + total: false, // optional ); diff --git a/docs/examples/functions/list-executions.md b/docs/examples/functions/list-executions.md index 232f3250..b4071bff 100644 --- a/docs/examples/functions/list-executions.md +++ b/docs/examples/functions/list-executions.md @@ -9,4 +9,5 @@ Functions functions = Functions(client); ExecutionList result = await functions.listExecutions( functionId: '', queries: [], // optional + total: false, // optional ); diff --git a/docs/examples/storage/list-files.md b/docs/examples/storage/list-files.md index 7950005b..8f7c3bd7 100644 --- a/docs/examples/storage/list-files.md +++ b/docs/examples/storage/list-files.md @@ -10,4 +10,5 @@ FileList result = await storage.listFiles( bucketId: '', queries: [], // optional search: '', // optional + total: false, // optional ); diff --git a/docs/examples/tablesdb/list-rows.md b/docs/examples/tablesdb/list-rows.md index 01d70665..38305101 100644 --- a/docs/examples/tablesdb/list-rows.md +++ b/docs/examples/tablesdb/list-rows.md @@ -11,4 +11,5 @@ RowList result = await tablesDB.listRows( tableId: '', queries: [], // optional transactionId: '', // optional + total: false, // optional ); diff --git a/docs/examples/teams/list-memberships.md b/docs/examples/teams/list-memberships.md index 374dd490..86b5eed2 100644 --- a/docs/examples/teams/list-memberships.md +++ b/docs/examples/teams/list-memberships.md @@ -10,4 +10,5 @@ MembershipList result = await teams.listMemberships( teamId: '', queries: [], // optional search: '', // optional + total: false, // optional ); diff --git a/docs/examples/teams/list.md b/docs/examples/teams/list.md index 3aa972fb..fd8b60f2 100644 --- a/docs/examples/teams/list.md +++ b/docs/examples/teams/list.md @@ -9,4 +9,5 @@ Teams teams = Teams(client); TeamList result = await teams.list( queries: [], // optional search: '', // optional + total: false, // optional ); diff --git a/lib/appwrite.dart b/lib/appwrite.dart index 62e9c5a8..692cef62 100644 --- a/lib/appwrite.dart +++ b/lib/appwrite.dart @@ -30,6 +30,7 @@ part 'query.dart'; part 'permission.dart'; part 'role.dart'; part 'id.dart'; +part 'operator.dart'; part 'services/account.dart'; part 'services/avatars.dart'; part 'services/databases.dart'; diff --git a/lib/operator.dart b/lib/operator.dart new file mode 100644 index 00000000..337dda30 --- /dev/null +++ b/lib/operator.dart @@ -0,0 +1,187 @@ +part of 'appwrite.dart'; + +/// Filter condition for array operations +enum Condition { + equal('equal'), + notEqual('notEqual'), + greaterThan('greaterThan'), + greaterThanEqual('greaterThanEqual'), + lessThan('lessThan'), + lessThanEqual('lessThanEqual'), + contains('contains'), + isNull('isNull'), + isNotNull('isNotNull'); + + final String value; + const Condition(this.value); + + @override + String toString() => value; +} + +/// Helper class to generate operator strings for atomic operations. +class Operator { + final String method; + final dynamic values; + + Operator._(this.method, [this.values = null]); + + Map toJson() { + final result = {}; + + result['method'] = method; + + if (values != null) { + result['values'] = values is List ? values : [values]; + } + + return result; + } + + @override + String toString() => jsonEncode(toJson()); + + /// Increment a numeric attribute by a specified value. + static String increment([num value = 1, num? max]) { + if (value.toDouble().isNaN || value.toDouble().isInfinite) { + throw ArgumentError('Value cannot be NaN or Infinity'); + } + if (max != null && (max.toDouble().isNaN || max.toDouble().isInfinite)) { + throw ArgumentError('Max cannot be NaN or Infinity'); + } + final values = [value]; + if (max != null) { + values.add(max); + } + return Operator._('increment', values).toString(); + } + + /// Decrement a numeric attribute by a specified value. + static String decrement([num value = 1, num? min]) { + if (value.toDouble().isNaN || value.toDouble().isInfinite) { + throw ArgumentError('Value cannot be NaN or Infinity'); + } + if (min != null && (min.toDouble().isNaN || min.toDouble().isInfinite)) { + throw ArgumentError('Min cannot be NaN or Infinity'); + } + final values = [value]; + if (min != null) { + values.add(min); + } + return Operator._('decrement', values).toString(); + } + + /// Multiply a numeric attribute by a specified factor. + static String multiply(num factor, [num? max]) { + if (factor.toDouble().isNaN || factor.toDouble().isInfinite) { + throw ArgumentError('Factor cannot be NaN or Infinity'); + } + if (max != null && (max.toDouble().isNaN || max.toDouble().isInfinite)) { + throw ArgumentError('Max cannot be NaN or Infinity'); + } + final values = [factor]; + if (max != null) { + values.add(max); + } + return Operator._('multiply', values).toString(); + } + + /// Divide a numeric attribute by a specified divisor. + static String divide(num divisor, [num? min]) { + if (divisor.toDouble().isNaN || divisor.toDouble().isInfinite) { + throw ArgumentError('Divisor cannot be NaN or Infinity'); + } + if (min != null && (min.toDouble().isNaN || min.toDouble().isInfinite)) { + throw ArgumentError('Min cannot be NaN or Infinity'); + } + if (divisor == 0) { + throw ArgumentError('Divisor cannot be zero'); + } + final values = [divisor]; + if (min != null) { + values.add(min); + } + return Operator._('divide', values).toString(); + } + + /// Apply modulo operation on a numeric attribute. + static String modulo(num divisor) { + if (divisor.toDouble().isNaN || divisor.toDouble().isInfinite) { + throw ArgumentError('Divisor cannot be NaN or Infinity'); + } + if (divisor == 0) { + throw ArgumentError('Divisor cannot be zero'); + } + return Operator._('modulo', [divisor]).toString(); + } + + /// Raise a numeric attribute to a specified power. + static String power(num exponent, [num? max]) { + if (exponent.toDouble().isNaN || exponent.toDouble().isInfinite) { + throw ArgumentError('Exponent cannot be NaN or Infinity'); + } + if (max != null && (max.toDouble().isNaN || max.toDouble().isInfinite)) { + throw ArgumentError('Max cannot be NaN or Infinity'); + } + final values = [exponent]; + if (max != null) { + values.add(max); + } + return Operator._('power', values).toString(); + } + + /// Append values to an array attribute. + static String arrayAppend(List values) => + Operator._('arrayAppend', values).toString(); + + /// Prepend values to an array attribute. + static String arrayPrepend(List values) => + Operator._('arrayPrepend', values).toString(); + + /// Insert a value at a specific index in an array attribute. + static String arrayInsert(int index, dynamic value) => + Operator._('arrayInsert', [index, value]).toString(); + + /// Remove a value from an array attribute. + static String arrayRemove(dynamic value) => + Operator._('arrayRemove', [value]).toString(); + + /// Remove duplicate values from an array attribute. + static String arrayUnique() => Operator._('arrayUnique', []).toString(); + + /// Keep only values that exist in both the current array and the provided array. + static String arrayIntersect(List values) => + Operator._('arrayIntersect', values).toString(); + + /// Remove values from the array that exist in the provided array. + static String arrayDiff(List values) => + Operator._('arrayDiff', values).toString(); + + /// Filter array values based on a condition. + static String arrayFilter(Condition condition, [dynamic value]) { + final values = [condition.value, value]; + return Operator._('arrayFilter', values).toString(); + } + + /// Concatenate a value to a string or array attribute. + static String stringConcat(dynamic value) => + Operator._('stringConcat', [value]).toString(); + + /// Replace occurrences of a search string with a replacement string. + static String stringReplace(String search, String replace) => + Operator._('stringReplace', [search, replace]).toString(); + + /// Toggle a boolean attribute. + static String toggle() => Operator._('toggle', []).toString(); + + /// Add days to a date attribute. + static String dateAddDays(int days) => + Operator._('dateAddDays', [days]).toString(); + + /// Subtract days from a date attribute. + static String dateSubDays(int days) => + Operator._('dateSubDays', [days]).toString(); + + /// Set a date attribute to the current date and time. + static String dateSetNow() => Operator._('dateSetNow', []).toString(); +} diff --git a/lib/query.dart b/lib/query.dart index d0b97b2d..07b6e353 100644 --- a/lib/query.dart +++ b/lib/query.dart @@ -106,28 +106,24 @@ class Query { Query._('notEndsWith', attribute, value).toString(); /// Filter resources where document was created before [value]. - static String createdBefore(String value) => - Query._('createdBefore', null, value).toString(); + static String createdBefore(String value) => lessThan('\$createdAt', value); /// Filter resources where document was created after [value]. - static String createdAfter(String value) => - Query._('createdAfter', null, value).toString(); + static String createdAfter(String value) => greaterThan('\$createdAt', value); /// Filter resources where document was created between [start] and [end] (inclusive). static String createdBetween(String start, String end) => - Query._('createdBetween', null, [start, end]).toString(); + between('\$createdAt', start, end); /// Filter resources where document was updated before [value]. - static String updatedBefore(String value) => - Query._('updatedBefore', null, value).toString(); + static String updatedBefore(String value) => lessThan('\$updatedAt', value); /// Filter resources where document was updated after [value]. - static String updatedAfter(String value) => - Query._('updatedAfter', null, value).toString(); + static String updatedAfter(String value) => greaterThan('\$updatedAt', value); /// Filter resources where document was updated between [start] and [end] (inclusive). static String updatedBetween(String start, String end) => - Query._('updatedBetween', null, [start, end]).toString(); + between('\$updatedAt', start, end); static String or(List queries) => Query._( 'or', diff --git a/lib/services/account.dart b/lib/services/account.dart index e24382f5..69e25d07 100644 --- a/lib/services/account.dart +++ b/lib/services/account.dart @@ -78,11 +78,13 @@ class Account extends Service { } /// Get the list of identities for the currently logged in user. - Future listIdentities({List? queries}) async { + Future listIdentities( + {List? queries, bool? total}) async { const String apiPath = '/account/identities'; final Map apiParams = { 'queries': queries, + 'total': total, }; final Map apiHeaders = {}; @@ -132,11 +134,12 @@ class Account extends Service { /// Get the list of latest security activity logs for the currently logged in /// user. Each log returns user IP address, location and date and time of log. - Future listLogs({List? queries}) async { + Future listLogs({List? queries, bool? total}) async { const String apiPath = '/account/logs'; final Map apiParams = { 'queries': queries, + 'total': total, }; final Map apiHeaders = {}; diff --git a/lib/services/databases.dart b/lib/services/databases.dart index 23bb0650..1f2f2b6f 100644 --- a/lib/services/databases.dart +++ b/lib/services/databases.dart @@ -123,7 +123,8 @@ class Databases extends Service { {required String databaseId, required String collectionId, List? queries, - String? transactionId}) async { + String? transactionId, + bool? total}) async { final String apiPath = '/databases/{databaseId}/collections/{collectionId}/documents' .replaceAll('{databaseId}', databaseId) @@ -132,6 +133,7 @@ class Databases extends Service { final Map apiParams = { 'queries': queries, 'transactionId': transactionId, + 'total': total, }; final Map apiHeaders = {}; diff --git a/lib/services/functions.dart b/lib/services/functions.dart index 8b8f9c1e..b7d9e754 100644 --- a/lib/services/functions.dart +++ b/lib/services/functions.dart @@ -9,12 +9,13 @@ class Functions extends Service { /// Get a list of all the current user function execution logs. You can use the /// query params to filter your results. Future listExecutions( - {required String functionId, List? queries}) async { + {required String functionId, List? queries, bool? total}) async { final String apiPath = '/functions/{functionId}/executions' .replaceAll('{functionId}', functionId); final Map apiParams = { 'queries': queries, + 'total': total, }; final Map apiHeaders = {}; diff --git a/lib/services/storage.dart b/lib/services/storage.dart index e3f141dc..de6ebb67 100644 --- a/lib/services/storage.dart +++ b/lib/services/storage.dart @@ -8,13 +8,17 @@ class Storage extends Service { /// Get a list of all the user files. You can use the query params to filter /// your results. Future listFiles( - {required String bucketId, List? queries, String? search}) async { + {required String bucketId, + List? queries, + String? search, + bool? total}) async { final String apiPath = '/storage/buckets/{bucketId}/files'.replaceAll('{bucketId}', bucketId); final Map apiParams = { 'queries': queries, 'search': search, + 'total': total, }; final Map apiHeaders = {}; diff --git a/lib/services/tables_db.dart b/lib/services/tables_db.dart index 702fe0e8..39a44707 100644 --- a/lib/services/tables_db.dart +++ b/lib/services/tables_db.dart @@ -119,7 +119,8 @@ class TablesDB extends Service { {required String databaseId, required String tableId, List? queries, - String? transactionId}) async { + String? transactionId, + bool? total}) async { final String apiPath = '/tablesdb/{databaseId}/tables/{tableId}/rows' .replaceAll('{databaseId}', databaseId) .replaceAll('{tableId}', tableId); @@ -127,6 +128,7 @@ class TablesDB extends Service { final Map apiParams = { 'queries': queries, 'transactionId': transactionId, + 'total': total, }; final Map apiHeaders = {}; diff --git a/lib/services/teams.dart b/lib/services/teams.dart index 4da8936a..879459e9 100644 --- a/lib/services/teams.dart +++ b/lib/services/teams.dart @@ -8,12 +8,14 @@ class Teams extends Service { /// Get a list of all the teams in which the current user is a member. You can /// use the parameters to filter your results. - Future list({List? queries, String? search}) async { + Future list( + {List? queries, String? search, bool? total}) async { const String apiPath = '/teams'; final Map apiParams = { 'queries': queries, 'search': search, + 'total': total, }; final Map apiHeaders = {}; @@ -103,13 +105,17 @@ class Teams extends Service { /// members have read access to this endpoint. Hide sensitive attributes from /// the response by toggling membership privacy in the Console. Future listMemberships( - {required String teamId, List? queries, String? search}) async { + {required String teamId, + List? queries, + String? search, + bool? total}) async { final String apiPath = '/teams/{teamId}/memberships'.replaceAll('{teamId}', teamId); final Map apiParams = { 'queries': queries, 'search': search, + 'total': total, }; final Map apiHeaders = {}; diff --git a/lib/src/client_browser.dart b/lib/src/client_browser.dart index cc1b02bd..dfc2360c 100644 --- a/lib/src/client_browser.dart +++ b/lib/src/client_browser.dart @@ -40,7 +40,7 @@ class ClientBrowser extends ClientBase with ClientMixin { 'x-sdk-name': 'Flutter', 'x-sdk-platform': 'client', 'x-sdk-language': 'flutter', - 'x-sdk-version': '20.2.2', + 'x-sdk-version': '20.3.0', 'X-Appwrite-Response-Format': '1.8.0', }; diff --git a/lib/src/client_io.dart b/lib/src/client_io.dart index f8e18055..ac581fc6 100644 --- a/lib/src/client_io.dart +++ b/lib/src/client_io.dart @@ -58,7 +58,7 @@ class ClientIO extends ClientBase with ClientMixin { 'x-sdk-name': 'Flutter', 'x-sdk-platform': 'client', 'x-sdk-language': 'flutter', - 'x-sdk-version': '20.2.2', + 'x-sdk-version': '20.3.0', 'X-Appwrite-Response-Format': '1.8.0', }; diff --git a/pubspec.yaml b/pubspec.yaml index b50467bd..76c973dd 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: appwrite -version: 20.2.2 +version: 20.3.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 diff --git a/test/operator_test.dart b/test/operator_test.dart new file mode 100644 index 00000000..9f14a5b1 --- /dev/null +++ b/test/operator_test.dart @@ -0,0 +1,156 @@ +import 'dart:convert'; + +import 'package:appwrite/appwrite.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('returns increment', () { + final op = jsonDecode(Operator.increment(1)); + expect(op['method'], 'increment'); + expect(op['values'], [1]); + }); + + test('returns increment with max', () { + final op = jsonDecode(Operator.increment(5, 100)); + expect(op['method'], 'increment'); + expect(op['values'], [5, 100]); + }); + + test('returns decrement', () { + final op = jsonDecode(Operator.decrement(1)); + expect(op['method'], 'decrement'); + expect(op['values'], [1]); + }); + + test('returns decrement with min', () { + final op = jsonDecode(Operator.decrement(3, 0)); + expect(op['method'], 'decrement'); + expect(op['values'], [3, 0]); + }); + + test('returns multiply', () { + final op = jsonDecode(Operator.multiply(2)); + expect(op['method'], 'multiply'); + expect(op['values'], [2]); + }); + + test('returns multiply with max', () { + final op = jsonDecode(Operator.multiply(3, 1000)); + expect(op['method'], 'multiply'); + expect(op['values'], [3, 1000]); + }); + + test('returns divide', () { + final op = jsonDecode(Operator.divide(2)); + expect(op['method'], 'divide'); + expect(op['values'], [2]); + }); + + test('returns divide with min', () { + final op = jsonDecode(Operator.divide(4, 1)); + expect(op['method'], 'divide'); + expect(op['values'], [4, 1]); + }); + + test('returns modulo', () { + final op = jsonDecode(Operator.modulo(5)); + expect(op['method'], 'modulo'); + expect(op['values'], [5]); + }); + + test('returns power', () { + final op = jsonDecode(Operator.power(2)); + expect(op['method'], 'power'); + expect(op['values'], [2]); + }); + + test('returns power with max', () { + final op = jsonDecode(Operator.power(3, 100)); + expect(op['method'], 'power'); + expect(op['values'], [3, 100]); + }); + + test('returns arrayAppend', () { + final op = jsonDecode(Operator.arrayAppend(['item1', 'item2'])); + expect(op['method'], 'arrayAppend'); + expect(op['values'], ['item1', 'item2']); + }); + + test('returns arrayPrepend', () { + final op = jsonDecode(Operator.arrayPrepend(['first', 'second'])); + expect(op['method'], 'arrayPrepend'); + expect(op['values'], ['first', 'second']); + }); + + test('returns arrayInsert', () { + final op = jsonDecode(Operator.arrayInsert(0, 'newItem')); + expect(op['method'], 'arrayInsert'); + expect(op['values'], [0, 'newItem']); + }); + + test('returns arrayRemove', () { + final op = jsonDecode(Operator.arrayRemove('oldItem')); + expect(op['method'], 'arrayRemove'); + expect(op['values'], ['oldItem']); + }); + + test('returns arrayUnique', () { + final op = jsonDecode(Operator.arrayUnique()); + expect(op['method'], 'arrayUnique'); + expect(op['values'], []); + }); + + test('returns arrayIntersect', () { + final op = jsonDecode(Operator.arrayIntersect(['a', 'b', 'c'])); + expect(op['method'], 'arrayIntersect'); + expect(op['values'], ['a', 'b', 'c']); + }); + + test('returns arrayDiff', () { + final op = jsonDecode(Operator.arrayDiff(['x', 'y'])); + expect(op['method'], 'arrayDiff'); + expect(op['values'], ['x', 'y']); + }); + + test('returns arrayFilter', () { + final op = jsonDecode(Operator.arrayFilter(Condition.equal, 'test')); + expect(op['method'], 'arrayFilter'); + expect(op['values'], ['equal', 'test']); + }); + + test('returns stringConcat', () { + final op = jsonDecode(Operator.stringConcat('suffix')); + expect(op['method'], 'stringConcat'); + expect(op['values'], ['suffix']); + }); + + test('returns stringReplace', () { + final op = jsonDecode(Operator.stringReplace('old', 'new')); + expect(op['method'], 'stringReplace'); + expect(op['values'], ['old', 'new']); + }); + + test('returns toggle', () { + final op = jsonDecode(Operator.toggle()); + expect(op['method'], 'toggle'); + expect(op['values'], []); + }); + + test('returns dateAddDays', () { + final op = jsonDecode(Operator.dateAddDays(7)); + expect(op['method'], 'dateAddDays'); + expect(op['values'], [7]); + }); + + test('returns dateSubDays', () { + final op = jsonDecode(Operator.dateSubDays(3)); + expect(op['method'], 'dateSubDays'); + expect(op['values'], [3]); + }); + + test('returns dateSetNow', () { + final op = jsonDecode(Operator.dateSetNow()); + expect(op['method'], 'dateSetNow'); + expect(op['values'], []); + }); +} diff --git a/test/query_test.dart b/test/query_test.dart index d7f2f5aa..0234a78a 100644 --- a/test/query_test.dart +++ b/test/query_test.dart @@ -274,43 +274,43 @@ void main() { test('returns createdBefore', () { final query = jsonDecode(Query.createdBefore('2023-01-01')); - expect(query['attribute'], null); + expect(query['attribute'], '\$createdAt'); expect(query['values'], ['2023-01-01']); - expect(query['method'], 'createdBefore'); + expect(query['method'], 'lessThan'); }); test('returns createdAfter', () { final query = jsonDecode(Query.createdAfter('2023-01-01')); - expect(query['attribute'], null); + expect(query['attribute'], '\$createdAt'); expect(query['values'], ['2023-01-01']); - expect(query['method'], 'createdAfter'); + expect(query['method'], 'greaterThan'); }); test('returns createdBetween', () { final query = jsonDecode(Query.createdBetween('2023-01-01', '2023-12-31')); - expect(query['attribute'], null); + expect(query['attribute'], '\$createdAt'); expect(query['values'], ['2023-01-01', '2023-12-31']); - expect(query['method'], 'createdBetween'); + expect(query['method'], 'between'); }); test('returns updatedBefore', () { final query = jsonDecode(Query.updatedBefore('2023-01-01')); - expect(query['attribute'], null); + expect(query['attribute'], '\$updatedAt'); expect(query['values'], ['2023-01-01']); - expect(query['method'], 'updatedBefore'); + expect(query['method'], 'lessThan'); }); test('returns updatedAfter', () { final query = jsonDecode(Query.updatedAfter('2023-01-01')); - expect(query['attribute'], null); + expect(query['attribute'], '\$updatedAt'); expect(query['values'], ['2023-01-01']); - expect(query['method'], 'updatedAfter'); + expect(query['method'], 'greaterThan'); }); test('returns updatedBetween', () { final query = jsonDecode(Query.updatedBetween('2023-01-01', '2023-12-31')); - expect(query['attribute'], null); + expect(query['attribute'], '\$updatedAt'); expect(query['values'], ['2023-01-01', '2023-12-31']); - expect(query['method'], 'updatedBetween'); + expect(query['method'], 'between'); }); }