Merge pull request #282 from appwrite/dev

feat: Flutter SDK update for version 20.3.0
This commit is contained in:
Jake Barnby
2025-11-03 18:46:56 +13:00
committed by GitHub
24 changed files with 405 additions and 34 deletions
+5
View File
@@ -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
+1 -1
View File
@@ -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:
+1
View File
@@ -8,4 +8,5 @@ Account account = Account(client);
IdentityList result = await account.listIdentities(
queries: [], // optional
total: false, // optional
);
+1
View File
@@ -8,4 +8,5 @@ Account account = Account(client);
LogList result = await account.listLogs(
queries: [], // optional
total: false, // optional
);
@@ -11,4 +11,5 @@ DocumentList result = await databases.listDocuments(
collectionId: '<COLLECTION_ID>',
queries: [], // optional
transactionId: '<TRANSACTION_ID>', // optional
total: false, // optional
);
@@ -9,4 +9,5 @@ Functions functions = Functions(client);
ExecutionList result = await functions.listExecutions(
functionId: '<FUNCTION_ID>',
queries: [], // optional
total: false, // optional
);
+1
View File
@@ -10,4 +10,5 @@ FileList result = await storage.listFiles(
bucketId: '<BUCKET_ID>',
queries: [], // optional
search: '<SEARCH>', // optional
total: false, // optional
);
+1
View File
@@ -11,4 +11,5 @@ RowList result = await tablesDB.listRows(
tableId: '<TABLE_ID>',
queries: [], // optional
transactionId: '<TRANSACTION_ID>', // optional
total: false, // optional
);
+1
View File
@@ -10,4 +10,5 @@ MembershipList result = await teams.listMemberships(
teamId: '<TEAM_ID>',
queries: [], // optional
search: '<SEARCH>', // optional
total: false, // optional
);
+1
View File
@@ -9,4 +9,5 @@ Teams teams = Teams(client);
TeamList result = await teams.list(
queries: [], // optional
search: '<SEARCH>', // optional
total: false, // optional
);
+1
View File
@@ -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';
+187
View File
@@ -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<String, dynamic> toJson() {
final result = <String, dynamic>{};
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 = <dynamic>[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 = <dynamic>[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 = <dynamic>[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 = <dynamic>[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 = <dynamic>[exponent];
if (max != null) {
values.add(max);
}
return Operator._('power', values).toString();
}
/// Append values to an array attribute.
static String arrayAppend(List<dynamic> values) =>
Operator._('arrayAppend', values).toString();
/// Prepend values to an array attribute.
static String arrayPrepend(List<dynamic> 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<dynamic> values) =>
Operator._('arrayIntersect', values).toString();
/// Remove values from the array that exist in the provided array.
static String arrayDiff(List<dynamic> values) =>
Operator._('arrayDiff', values).toString();
/// Filter array values based on a condition.
static String arrayFilter(Condition condition, [dynamic value]) {
final values = <dynamic>[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();
}
+6 -10
View File
@@ -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<String> queries) => Query._(
'or',
+5 -2
View File
@@ -78,11 +78,13 @@ class Account extends Service {
}
/// Get the list of identities for the currently logged in user.
Future<models.IdentityList> listIdentities({List<String>? queries}) async {
Future<models.IdentityList> listIdentities(
{List<String>? queries, bool? total}) async {
const String apiPath = '/account/identities';
final Map<String, dynamic> apiParams = {
'queries': queries,
'total': total,
};
final Map<String, String> 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<models.LogList> listLogs({List<String>? queries}) async {
Future<models.LogList> listLogs({List<String>? queries, bool? total}) async {
const String apiPath = '/account/logs';
final Map<String, dynamic> apiParams = {
'queries': queries,
'total': total,
};
final Map<String, String> apiHeaders = {};
+3 -1
View File
@@ -123,7 +123,8 @@ class Databases extends Service {
{required String databaseId,
required String collectionId,
List<String>? 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<String, dynamic> apiParams = {
'queries': queries,
'transactionId': transactionId,
'total': total,
};
final Map<String, String> apiHeaders = {};
+2 -1
View File
@@ -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<models.ExecutionList> listExecutions(
{required String functionId, List<String>? queries}) async {
{required String functionId, List<String>? queries, bool? total}) async {
final String apiPath = '/functions/{functionId}/executions'
.replaceAll('{functionId}', functionId);
final Map<String, dynamic> apiParams = {
'queries': queries,
'total': total,
};
final Map<String, String> apiHeaders = {};
+5 -1
View File
@@ -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<models.FileList> listFiles(
{required String bucketId, List<String>? queries, String? search}) async {
{required String bucketId,
List<String>? queries,
String? search,
bool? total}) async {
final String apiPath =
'/storage/buckets/{bucketId}/files'.replaceAll('{bucketId}', bucketId);
final Map<String, dynamic> apiParams = {
'queries': queries,
'search': search,
'total': total,
};
final Map<String, String> apiHeaders = {};
+3 -1
View File
@@ -119,7 +119,8 @@ class TablesDB extends Service {
{required String databaseId,
required String tableId,
List<String>? 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<String, dynamic> apiParams = {
'queries': queries,
'transactionId': transactionId,
'total': total,
};
final Map<String, String> apiHeaders = {};
+8 -2
View File
@@ -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<models.TeamList> list({List<String>? queries, String? search}) async {
Future<models.TeamList> list(
{List<String>? queries, String? search, bool? total}) async {
const String apiPath = '/teams';
final Map<String, dynamic> apiParams = {
'queries': queries,
'search': search,
'total': total,
};
final Map<String, String> 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<models.MembershipList> listMemberships(
{required String teamId, List<String>? queries, String? search}) async {
{required String teamId,
List<String>? queries,
String? search,
bool? total}) async {
final String apiPath =
'/teams/{teamId}/memberships'.replaceAll('{teamId}', teamId);
final Map<String, dynamic> apiParams = {
'queries': queries,
'search': search,
'total': total,
};
final Map<String, String> apiHeaders = {};
+1 -1
View File
@@ -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',
};
+1 -1
View File
@@ -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',
};
+1 -1
View File
@@ -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
+156
View File
@@ -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'], []);
});
}
+12 -12
View File
@@ -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');
});
}