diff --git a/CHANGELOG.md b/CHANGELOG.md index 3be014df..9d0f2fa8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Change Log +## 21.0.0 + +* Add array-based enum parameters (e.g., `permissions: List`). +* Breaking change: `Output` enum has been removed; use `ImageFormat` instead. +* Add `Channel` helpers for Realtime. + ## 20.3.3 * Fix boolean parameter not handled correctly in Client requests diff --git a/README.md b/README.md index d433331e..a42d0372 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ [![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 latest. For older versions, please check [previous releases](https://github.com/appwrite/sdk-for-flutter/releases).** +**This SDK is compatible with Appwrite server version 1.8.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 abstracts and simplifies 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) @@ -19,7 +19,7 @@ Add this to your package's `pubspec.yaml` file: ```yml dependencies: - appwrite: ^20.3.3 + appwrite: ^21.0.0 ``` You can install packages from the command line: diff --git a/docs/examples/account/create-mfa-authenticator.md b/docs/examples/account/create-mfa-authenticator.md index b9d7e967..2cb96dff 100644 --- a/docs/examples/account/create-mfa-authenticator.md +++ b/docs/examples/account/create-mfa-authenticator.md @@ -7,5 +7,5 @@ Client client = Client() Account account = Account(client); MfaType result = await account.createMFAAuthenticator( - type: AuthenticatorType.totp, + type: enums.AuthenticatorType.totp, ); diff --git a/docs/examples/account/create-mfa-challenge.md b/docs/examples/account/create-mfa-challenge.md index 09ce17b2..8e7d1668 100644 --- a/docs/examples/account/create-mfa-challenge.md +++ b/docs/examples/account/create-mfa-challenge.md @@ -7,5 +7,5 @@ Client client = Client() Account account = Account(client); MfaChallenge result = await account.createMFAChallenge( - factor: AuthenticationFactor.email, + factor: enums.AuthenticationFactor.email, ); diff --git a/docs/examples/account/create-o-auth-2-session.md b/docs/examples/account/create-o-auth-2-session.md index ab53f4a7..c013d90b 100644 --- a/docs/examples/account/create-o-auth-2-session.md +++ b/docs/examples/account/create-o-auth-2-session.md @@ -7,7 +7,7 @@ Client client = Client() Account account = Account(client); await account.createOAuth2Session( - provider: OAuthProvider.amazon, + provider: enums.OAuthProvider.amazon, success: 'https://example.com', // optional failure: 'https://example.com', // optional scopes: [], // optional diff --git a/docs/examples/account/create-o-auth-2-token.md b/docs/examples/account/create-o-auth-2-token.md index d6b6c72c..2b2d246e 100644 --- a/docs/examples/account/create-o-auth-2-token.md +++ b/docs/examples/account/create-o-auth-2-token.md @@ -7,7 +7,7 @@ Client client = Client() Account account = Account(client); await account.createOAuth2Token( - provider: OAuthProvider.amazon, + provider: enums.OAuthProvider.amazon, success: 'https://example.com', // optional failure: 'https://example.com', // optional scopes: [], // optional diff --git a/docs/examples/account/delete-mfa-authenticator.md b/docs/examples/account/delete-mfa-authenticator.md index b938ca68..0ed6d5bc 100644 --- a/docs/examples/account/delete-mfa-authenticator.md +++ b/docs/examples/account/delete-mfa-authenticator.md @@ -7,5 +7,5 @@ Client client = Client() Account account = Account(client); await account.deleteMFAAuthenticator( - type: AuthenticatorType.totp, + type: enums.AuthenticatorType.totp, ); diff --git a/docs/examples/account/update-mfa-authenticator.md b/docs/examples/account/update-mfa-authenticator.md index 96bdcc1b..641ae8fb 100644 --- a/docs/examples/account/update-mfa-authenticator.md +++ b/docs/examples/account/update-mfa-authenticator.md @@ -7,6 +7,6 @@ Client client = Client() Account account = Account(client); User result = await account.updateMFAAuthenticator( - type: AuthenticatorType.totp, + type: enums.AuthenticatorType.totp, otp: '', ); diff --git a/docs/examples/avatars/get-browser.md b/docs/examples/avatars/get-browser.md index 50c28ff3..7528a622 100644 --- a/docs/examples/avatars/get-browser.md +++ b/docs/examples/avatars/get-browser.md @@ -8,7 +8,7 @@ Avatars avatars = Avatars(client); // Downloading file Uint8List bytes = await avatars.getBrowser( - code: Browser.avantBrowser, + code: enums.Browser.avantBrowser, width: 0, // optional height: 0, // optional quality: -1, // optional @@ -20,7 +20,7 @@ file.writeAsBytesSync(bytes); // Displaying image preview FutureBuilder( future: avatars.getBrowser( - code: Browser.avantBrowser, + code: enums.Browser.avantBrowser, width:0 , // optional height:0 , // optional quality:-1 , // optional diff --git a/docs/examples/avatars/get-credit-card.md b/docs/examples/avatars/get-credit-card.md index c3471fc2..2586613c 100644 --- a/docs/examples/avatars/get-credit-card.md +++ b/docs/examples/avatars/get-credit-card.md @@ -8,7 +8,7 @@ Avatars avatars = Avatars(client); // Downloading file Uint8List bytes = await avatars.getCreditCard( - code: CreditCard.americanExpress, + code: enums.CreditCard.americanExpress, width: 0, // optional height: 0, // optional quality: -1, // optional @@ -20,7 +20,7 @@ file.writeAsBytesSync(bytes); // Displaying image preview FutureBuilder( future: avatars.getCreditCard( - code: CreditCard.americanExpress, + code: enums.CreditCard.americanExpress, width:0 , // optional height:0 , // optional quality:-1 , // optional diff --git a/docs/examples/avatars/get-flag.md b/docs/examples/avatars/get-flag.md index e5a4c60b..8d2649b3 100644 --- a/docs/examples/avatars/get-flag.md +++ b/docs/examples/avatars/get-flag.md @@ -8,7 +8,7 @@ Avatars avatars = Avatars(client); // Downloading file Uint8List bytes = await avatars.getFlag( - code: Flag.afghanistan, + code: enums.Flag.afghanistan, width: 0, // optional height: 0, // optional quality: -1, // optional @@ -20,7 +20,7 @@ file.writeAsBytesSync(bytes); // Displaying image preview FutureBuilder( future: avatars.getFlag( - code: Flag.afghanistan, + code: enums.Flag.afghanistan, width:0 , // optional height:0 , // optional quality:-1 , // optional diff --git a/docs/examples/avatars/get-screenshot.md b/docs/examples/avatars/get-screenshot.md index ed44613f..43c77ca2 100644 --- a/docs/examples/avatars/get-screenshot.md +++ b/docs/examples/avatars/get-screenshot.md @@ -16,21 +16,21 @@ Uint8List bytes = await avatars.getScreenshot( viewportWidth: 1920, // optional viewportHeight: 1080, // optional scale: 2, // optional - theme: Theme.light, // optional + theme: enums.Theme.dark, // optional userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15', // optional fullpage: true, // optional locale: 'en-US', // optional - timezone: Timezone.africaAbidjan, // optional + timezone: enums.Timezone.americaNewYork, // optional latitude: 37.7749, // optional longitude: -122.4194, // optional accuracy: 100, // optional touch: true, // optional - permissions: ["geolocation","notifications"], // optional + permissions: [enums.BrowserPermission.geolocation, enums.BrowserPermission.notifications], // optional sleep: 3, // optional width: 800, // optional height: 600, // optional quality: 85, // optional - output: ImageFormat.jpg, // optional + output: enums.ImageFormat.jpeg, // optional ) final file = File('path_to_file/filename.ext'); @@ -47,21 +47,21 @@ FutureBuilder( viewportWidth:1920 , // optional viewportHeight:1080 , // optional scale:2 , // optional - theme: Theme.light, // optional + theme: enums.Theme.dark, // optional userAgent:'Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15' , // optional fullpage:true , // optional locale:'en-US' , // optional - timezone: Timezone.africaAbidjan, // optional + timezone: enums.Timezone.americaNewYork, // optional latitude:37.7749 , // optional longitude:-122.4194 , // optional accuracy:100 , // optional touch:true , // optional - permissions:["geolocation","notifications"] , // optional + permissions: [enums.BrowserPermission.geolocation, enums.BrowserPermission.notifications], // optional sleep:3 , // optional width:800 , // optional height:600 , // optional quality:85 , // optional - output: ImageFormat.jpg, // optional + output: enums.ImageFormat.jpeg, // optional ), // Works for both public file and private file, for private files you need to be logged in builder: (context, snapshot) { return snapshot.hasData && snapshot.data != null diff --git a/docs/examples/functions/create-execution.md b/docs/examples/functions/create-execution.md index bbd7cd37..fa9780ff 100644 --- a/docs/examples/functions/create-execution.md +++ b/docs/examples/functions/create-execution.md @@ -11,7 +11,7 @@ Execution result = await functions.createExecution( body: '', // optional xasync: false, // optional path: '', // optional - method: ExecutionMethod.gET, // optional + method: enums.ExecutionMethod.gET, // optional headers: {}, // optional scheduledAt: '', // optional ); diff --git a/docs/examples/storage/get-file-preview.md b/docs/examples/storage/get-file-preview.md index 6fd148e9..92bee14e 100644 --- a/docs/examples/storage/get-file-preview.md +++ b/docs/examples/storage/get-file-preview.md @@ -12,7 +12,7 @@ Uint8List bytes = await storage.getFilePreview( fileId: '', width: 0, // optional height: 0, // optional - gravity: ImageGravity.center, // optional + gravity: enums.ImageGravity.center, // optional quality: -1, // optional borderWidth: 0, // optional borderColor: '', // optional @@ -20,7 +20,7 @@ Uint8List bytes = await storage.getFilePreview( opacity: 0, // optional rotation: -360, // optional background: '', // optional - output: ImageFormat.jpg, // optional + output: enums.ImageFormat.jpg, // optional token: '', // optional ) @@ -34,7 +34,7 @@ FutureBuilder( fileId:'' , width:0 , // optional height:0 , // optional - gravity: ImageGravity.center, // optional + gravity: enums.ImageGravity.center, // optional quality:-1 , // optional borderWidth:0 , // optional borderColor:'' , // optional @@ -42,7 +42,7 @@ FutureBuilder( opacity:0 , // optional rotation:-360 , // optional background:'' , // optional - output: ImageFormat.jpg, // optional + output: enums.ImageFormat.jpg, // optional token:'' , // optional ), // Works for both public file and private file, for private files you need to be logged in builder: (context, snapshot) { diff --git a/docs/examples/teams/create-membership.md b/docs/examples/teams/create-membership.md index 80265214..2491fba7 100644 --- a/docs/examples/teams/create-membership.md +++ b/docs/examples/teams/create-membership.md @@ -8,7 +8,7 @@ Teams teams = Teams(client); Membership result = await teams.createMembership( teamId: '', - roles: [], + roles: [enums.Roles.admin], email: 'email@example.com', // optional userId: '', // optional phone: '+12065550100', // optional diff --git a/docs/examples/teams/update-membership.md b/docs/examples/teams/update-membership.md index bc2bbae7..2b9f9b3e 100644 --- a/docs/examples/teams/update-membership.md +++ b/docs/examples/teams/update-membership.md @@ -9,5 +9,5 @@ Teams teams = Teams(client); Membership result = await teams.updateMembership( teamId: '', membershipId: '', - roles: [], + roles: [enums.Roles.admin], ); diff --git a/lib/appwrite.dart b/lib/appwrite.dart index 692cef62..c4f99a34 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 'channel.dart'; part 'operator.dart'; part 'services/account.dart'; part 'services/avatars.dart'; diff --git a/lib/channel.dart b/lib/channel.dart new file mode 100644 index 00000000..b83614be --- /dev/null +++ b/lib/channel.dart @@ -0,0 +1,171 @@ +part of appwrite; + +// Marker classes for type safety +class _Root {} + +class _Database {} + +class _Collection {} + +class _Document {} + +class _TablesDB {} + +class _Table {} + +class _Row {} + +class _Bucket {} + +class _File {} + +class _Func {} + +class _Execution {} + +class _Team {} + +class _Membership {} + +class _Resolved {} + +// Helper function for normalizing ID +String _normalize(String id) => id.trim().isEmpty ? '*' : id.trim(); + +/// Channel class with generic type parameter for type-safe method chaining +class Channel { + final List _segments; + + Channel._(this._segments); + + /// Internal helper to transition to next state with segment and ID + Channel _next(String segment, [String id = '*']) { + return Channel._([..._segments, segment, _normalize(id)]); + } + + /// Internal helper for terminal actions (no ID segment) + Channel<_Resolved> _resolve(String action) { + return Channel<_Resolved>._([..._segments, action]); + } + + @override + String toString() => _segments.join('.'); + + // --- ROOT FACTORIES --- + static Channel<_Database> database([String id = '*']) => + Channel<_Database>._(['databases', _normalize(id)]); + + static Channel<_TablesDB> tablesdb([String id = '*']) => + Channel<_TablesDB>._(['tablesdb', _normalize(id)]); + + static Channel<_Bucket> bucket([String id = '*']) => + Channel<_Bucket>._(['buckets', _normalize(id)]); + + static Channel<_Func> function([String id = '*']) => + Channel<_Func>._(['functions', _normalize(id)]); + + static Channel<_Team> team([String id = '*']) => + Channel<_Team>._(['teams', _normalize(id)]); + + static Channel<_Membership> membership([String id = '*']) => + Channel<_Membership>._(['memberships', _normalize(id)]); + + static String account([String userId = '']) { + final id = _normalize(userId); + return id == '*' ? 'account' : 'account.$id'; + } + + // Global events + static String documents() => 'documents'; + static String rows() => 'rows'; + static String files() => 'files'; + static String executions() => 'executions'; + static String teams() => 'teams'; +} + +// --- DATABASE ROUTE --- +// Extension methods restricted by receiver type + +/// Only available on Channel<_Database> +extension DatabaseChannel on Channel<_Database> { + Channel<_Collection> collection([String id = '*']) => + _next<_Collection>('collections', id); +} + +/// Only available on Channel<_Collection> +extension CollectionChannel on Channel<_Collection> { + Channel<_Document> document([String id = '*']) => + _next<_Document>('documents', id); +} + +// --- TABLESDB ROUTE --- + +/// Only available on Channel<_TablesDB> +extension TablesDBChannel on Channel<_TablesDB> { + Channel<_Table> table([String id = '*']) => _next<_Table>('tables', id); +} + +/// Only available on Channel<_Table> +extension TableChannel on Channel<_Table> { + Channel<_Row> row([String id = '*']) => _next<_Row>('rows', id); +} + +// --- BUCKET ROUTE --- + +/// Only available on Channel<_Bucket> +extension BucketChannel on Channel<_Bucket> { + Channel<_File> file([String id = '*']) => _next<_File>('files', id); +} + +// --- FUNCTION ROUTE --- + +/// Only available on Channel<_Func> +extension FuncChannel on Channel<_Func> { + Channel<_Execution> execution([String id = '*']) => + _next<_Execution>('executions', id); +} + +// --- TERMINAL ACTIONS --- +// Restricted to actionable types (_Document, _Row, _File, _Execution, _Team, _Membership) + +/// Only available on Channel<_Document> +extension DocumentChannel on Channel<_Document> { + Channel<_Resolved> create() => _resolve('create'); + Channel<_Resolved> update() => _resolve('update'); + Channel<_Resolved> delete() => _resolve('delete'); +} + +/// Only available on Channel<_Row> +extension RowChannel on Channel<_Row> { + Channel<_Resolved> create() => _resolve('create'); + Channel<_Resolved> update() => _resolve('update'); + Channel<_Resolved> delete() => _resolve('delete'); +} + +/// Only available on Channel<_File> +extension FileChannel on Channel<_File> { + Channel<_Resolved> create() => _resolve('create'); + Channel<_Resolved> update() => _resolve('update'); + Channel<_Resolved> delete() => _resolve('delete'); +} + +/// Only available on Channel<_Execution> +extension ExecutionChannel on Channel<_Execution> { + Channel<_Resolved> create() => _resolve('create'); + Channel<_Resolved> update() => _resolve('update'); + Channel<_Resolved> delete() => _resolve('delete'); +} + +/// Only available on Channel<_Team> +extension TeamChannel on Channel<_Team> { + Channel<_Resolved> create() => _resolve('create'); + Channel<_Resolved> update() => _resolve('update'); + Channel<_Resolved> delete() => _resolve('delete'); +} + +/// Only available on Channel<_Membership> +extension MembershipChannel on Channel<_Membership> { + Channel<_Resolved> create() => _resolve('create'); + Channel<_Resolved> update() => _resolve('update'); + Channel<_Resolved> delete() => _resolve('delete'); +} diff --git a/lib/enums.dart b/lib/enums.dart index 3429ca03..f2f5823f 100644 --- a/lib/enums.dart +++ b/lib/enums.dart @@ -9,8 +9,10 @@ part 'src/enums/credit_card.dart'; part 'src/enums/flag.dart'; part 'src/enums/theme.dart'; part 'src/enums/timezone.dart'; +part 'src/enums/browser_permission.dart'; part 'src/enums/image_format.dart'; part 'src/enums/execution_method.dart'; part 'src/enums/image_gravity.dart'; +part 'src/enums/roles.dart'; part 'src/enums/execution_trigger.dart'; part 'src/enums/execution_status.dart'; diff --git a/lib/query.dart b/lib/query.dart index 07b6e353..2f07d998 100644 --- a/lib/query.dart +++ b/lib/query.dart @@ -39,6 +39,13 @@ class Query { static String notEqual(String attribute, dynamic value) => Query._('notEqual', attribute, value).toString(); + /// Filter resources where [attribute] matches a regular expression pattern. + /// + /// [attribute] The attribute to filter on. + /// [pattern] The regular expression pattern to match. + static String regex(String attribute, String pattern) => + Query._('regex', attribute, pattern).toString(); + /// Filter resources where [attribute] is less than [value]. static String lessThan(String attribute, dynamic value) => Query._('lessThan', attribute, value).toString(); @@ -67,6 +74,18 @@ class Query { static String isNotNull(String attribute) => Query._('isNotNull', attribute).toString(); + /// Filter resources where the specified attributes exist. + /// + /// [attributes] The list of attributes that must exist. + static String exists(List attributes) => + Query._('exists', null, attributes).toString(); + + /// Filter resources where the specified attributes do not exist. + /// + /// [attributes] The list of attributes that must not exist. + static String notExists(List attributes) => + Query._('notExists', null, attributes).toString(); + /// Filter resources where [attribute] is between [start] and [end] (inclusive). static String between(String attribute, dynamic start, dynamic end) => Query._('between', attribute, [start, end]).toString(); @@ -137,6 +156,16 @@ class Query { queries.map((query) => jsonDecode(query)).toList(), ).toString(); + /// Filter array elements where at least one element matches all the specified queries. + /// + /// [attribute] The attribute containing the array to filter on. + /// [queries] The list of query strings to match against array elements. + static String elemMatch(String attribute, List queries) => Query._( + 'elemMatch', + attribute, + queries.map((query) => jsonDecode(query)).toList(), + ).toString(); + /// Specify which attributes should be returned by the API call. static String select(List attributes) => Query._('select', null, attributes).toString(); diff --git a/lib/services/avatars.dart b/lib/services/avatars.dart index 8921a8be..856714da 100644 --- a/lib/services/avatars.dart +++ b/lib/services/avatars.dart @@ -214,7 +214,7 @@ class Avatars extends Service { double? longitude, double? accuracy, bool? touch, - List? permissions, + List? permissions, int? sleep, int? width, int? height, @@ -228,21 +228,22 @@ class Avatars extends Service { if (viewportWidth != null) 'viewportWidth': viewportWidth, if (viewportHeight != null) 'viewportHeight': viewportHeight, if (scale != null) 'scale': scale, - if (theme != null) 'theme': theme!.value, + if (theme != null) 'theme': theme.value, if (userAgent != null) 'userAgent': userAgent, if (fullpage != null) 'fullpage': fullpage, if (locale != null) 'locale': locale, - if (timezone != null) 'timezone': timezone!.value, + if (timezone != null) 'timezone': timezone.value, if (latitude != null) 'latitude': latitude, if (longitude != null) 'longitude': longitude, if (accuracy != null) 'accuracy': accuracy, if (touch != null) 'touch': touch, - if (permissions != null) 'permissions': permissions, + if (permissions != null) + 'permissions': permissions.map((e) => e.value).toList(), if (sleep != null) 'sleep': sleep, if (width != null) 'width': width, if (height != null) 'height': height, if (quality != null) 'quality': quality, - if (output != null) 'output': output!.value, + if (output != null) 'output': output.value, 'project': client.config['project'], }; diff --git a/lib/services/functions.dart b/lib/services/functions.dart index c2cdb975..6fcd5075 100644 --- a/lib/services/functions.dart +++ b/lib/services/functions.dart @@ -45,7 +45,7 @@ class Functions extends Service { if (body != null) 'body': body, if (xasync != null) 'async': xasync, if (path != null) 'path': path, - if (method != null) 'method': method!.value, + if (method != null) 'method': method.value, if (headers != null) 'headers': headers, 'scheduledAt': scheduledAt, }; diff --git a/lib/services/storage.dart b/lib/services/storage.dart index 7ddfcf15..89f6999f 100644 --- a/lib/services/storage.dart +++ b/lib/services/storage.dart @@ -190,7 +190,7 @@ class Storage extends Service { final Map params = { if (width != null) 'width': width, if (height != null) 'height': height, - if (gravity != null) 'gravity': gravity!.value, + if (gravity != null) 'gravity': gravity.value, if (quality != null) 'quality': quality, if (borderWidth != null) 'borderWidth': borderWidth, if (borderColor != null) 'borderColor': borderColor, @@ -198,7 +198,7 @@ class Storage extends Service { if (opacity != null) 'opacity': opacity, if (rotation != null) 'rotation': rotation, if (background != null) 'background': background, - if (output != null) 'output': output!.value, + if (output != null) 'output': output.value, if (token != null) 'token': token, 'project': client.config['project'], }; diff --git a/lib/services/teams.dart b/lib/services/teams.dart index 6b4d5e39..988efd3e 100644 --- a/lib/services/teams.dart +++ b/lib/services/teams.dart @@ -149,7 +149,7 @@ class Teams extends Service { /// Future createMembership( {required String teamId, - required List roles, + required List roles, String? email, String? userId, String? phone, @@ -162,7 +162,7 @@ class Teams extends Service { if (email != null) 'email': email, if (userId != null) 'userId': userId, if (phone != null) 'phone': phone, - 'roles': roles, + 'roles': roles.map((e) => e.value).toList(), if (url != null) 'url': url, if (name != null) 'name': name, }; @@ -203,13 +203,13 @@ class Teams extends Service { Future updateMembership( {required String teamId, required String membershipId, - required List roles}) async { + required List roles}) async { final String apiPath = '/teams/{teamId}/memberships/{membershipId}' .replaceAll('{teamId}', teamId) .replaceAll('{membershipId}', membershipId); final Map apiParams = { - 'roles': roles, + 'roles': roles.map((e) => e.value).toList(), }; final Map apiHeaders = { diff --git a/lib/src/client_browser.dart b/lib/src/client_browser.dart index 7d467399..b5eea74f 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.3.3', + 'x-sdk-version': '21.0.0', 'X-Appwrite-Response-Format': '1.8.0', }; diff --git a/lib/src/client_io.dart b/lib/src/client_io.dart index 322f1308..c06eb95d 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.3.3', + 'x-sdk-version': '21.0.0', 'X-Appwrite-Response-Format': '1.8.0', }; diff --git a/lib/src/enums/browser_permission.dart b/lib/src/enums/browser_permission.dart new file mode 100644 index 00000000..fb4983db --- /dev/null +++ b/lib/src/enums/browser_permission.dart @@ -0,0 +1,30 @@ +part of '../../enums.dart'; + +enum BrowserPermission { + geolocation(value: 'geolocation'), + camera(value: 'camera'), + microphone(value: 'microphone'), + notifications(value: 'notifications'), + midi(value: 'midi'), + push(value: 'push'), + clipboardRead(value: 'clipboard-read'), + clipboardWrite(value: 'clipboard-write'), + paymentHandler(value: 'payment-handler'), + usb(value: 'usb'), + bluetooth(value: 'bluetooth'), + accelerometer(value: 'accelerometer'), + gyroscope(value: 'gyroscope'), + magnetometer(value: 'magnetometer'), + ambientLightSensor(value: 'ambient-light-sensor'), + backgroundSync(value: 'background-sync'), + persistentStorage(value: 'persistent-storage'), + screenWakeLock(value: 'screen-wake-lock'), + webShare(value: 'web-share'), + xrSpatialTracking(value: 'xr-spatial-tracking'); + + const BrowserPermission({required this.value}); + + final String value; + + String toJson() => value; +} diff --git a/lib/src/enums/o_auth_provider.dart b/lib/src/enums/o_auth_provider.dart index 48b6c205..fe3f1c74 100644 --- a/lib/src/enums/o_auth_provider.dart +++ b/lib/src/enums/o_auth_provider.dart @@ -39,9 +39,7 @@ enum OAuthProvider { yammer(value: 'yammer'), yandex(value: 'yandex'), zoho(value: 'zoho'), - zoom(value: 'zoom'), - mock(value: 'mock'), - mockUnverified(value: 'mock-unverified'); + zoom(value: 'zoom'); const OAuthProvider({required this.value}); diff --git a/lib/src/enums/roles.dart b/lib/src/enums/roles.dart new file mode 100644 index 00000000..af11fab2 --- /dev/null +++ b/lib/src/enums/roles.dart @@ -0,0 +1,13 @@ +part of '../../enums.dart'; + +enum Roles { + admin(value: 'admin'), + developer(value: 'developer'), + owner(value: 'owner'); + + const Roles({required this.value}); + + final String value; + + String toJson() => value; +} diff --git a/lib/src/models/file.dart b/lib/src/models/file.dart index 2df0de41..d17a8aac 100644 --- a/lib/src/models/file.dart +++ b/lib/src/models/file.dart @@ -35,6 +35,12 @@ class File implements Model { /// Total number of chunks uploaded final int chunksUploaded; + /// Whether file contents are encrypted at rest. + final bool encryption; + + /// Compression algorithm used for the file. Will be one of none, [gzip](https://en.wikipedia.org/wiki/Gzip), or [zstd](https://en.wikipedia.org/wiki/Zstd). + final String compression; + File({ required this.$id, required this.bucketId, @@ -47,6 +53,8 @@ class File implements Model { required this.sizeOriginal, required this.chunksTotal, required this.chunksUploaded, + required this.encryption, + required this.compression, }); factory File.fromMap(Map map) { @@ -62,6 +70,8 @@ class File implements Model { sizeOriginal: map['sizeOriginal'], chunksTotal: map['chunksTotal'], chunksUploaded: map['chunksUploaded'], + encryption: map['encryption'], + compression: map['compression'].toString(), ); } @@ -79,6 +89,8 @@ class File implements Model { "sizeOriginal": sizeOriginal, "chunksTotal": chunksTotal, "chunksUploaded": chunksUploaded, + "encryption": encryption, + "compression": compression, }; } } diff --git a/lib/src/realtime.dart b/lib/src/realtime.dart index 35f68677..19114f08 100644 --- a/lib/src/realtime.dart +++ b/lib/src/realtime.dart @@ -42,7 +42,16 @@ abstract class Realtime extends Service { /// subscription.close(); /// ``` /// - RealtimeSubscription subscribe(List channels); + /// You can also use Channel builders: + /// ```dart + /// final subscription = realtime.subscribe([ + /// Channel.database('db').collection('col').document('doc').create(), + /// Channel.bucket('bucket').file('file').update(), + /// 'account.*' + /// ]); + /// ``` + RealtimeSubscription subscribe( + List channels); // Object can be String or Channel /// The [close code](https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.5) set when the WebSocket connection is closed. /// diff --git a/lib/src/realtime_base.dart b/lib/src/realtime_base.dart index f9fe5e44..e9dd5aa2 100644 --- a/lib/src/realtime_base.dart +++ b/lib/src/realtime_base.dart @@ -3,5 +3,6 @@ import 'realtime.dart'; abstract class RealtimeBase implements Realtime { @override - RealtimeSubscription subscribe(List channels); + RealtimeSubscription subscribe( + List channels); // Object can be String or Channel } diff --git a/lib/src/realtime_browser.dart b/lib/src/realtime_browser.dart index aa6a3ad1..5d088906 100644 --- a/lib/src/realtime_browser.dart +++ b/lib/src/realtime_browser.dart @@ -35,7 +35,7 @@ class RealtimeBrowser extends RealtimeBase with RealtimeMixin { } @override - RealtimeSubscription subscribe(List channels) { + RealtimeSubscription subscribe(List channels) { return subscribeTo(channels); } } diff --git a/lib/src/realtime_io.dart b/lib/src/realtime_io.dart index 5b3454fa..92de37f3 100644 --- a/lib/src/realtime_io.dart +++ b/lib/src/realtime_io.dart @@ -43,7 +43,7 @@ class RealtimeIO extends RealtimeBase with RealtimeMixin { /// Use this method to subscribe to a channels and listen to /// realtime events on those channels @override - RealtimeSubscription subscribe(List channels) { + RealtimeSubscription subscribe(List channels) { return subscribeTo(channels); } diff --git a/lib/src/realtime_mixin.dart b/lib/src/realtime_mixin.dart index 555bae22..7a5aa6b8 100644 --- a/lib/src/realtime_mixin.dart +++ b/lib/src/realtime_mixin.dart @@ -166,18 +166,26 @@ mixin RealtimeMixin { ); } - RealtimeSubscription subscribeTo(List channels) { + /// Convert channel value to string + /// Handles String and Channel instances (which have toString()) + String _channelToString(Object channel) { + return channel is String ? channel : channel.toString(); + } + + RealtimeSubscription subscribeTo(List channels) { StreamController controller = StreamController.broadcast(); - _channels.addAll(channels); + final channelStrings = + channels.map((ch) => _channelToString(ch)).toList().cast(); + _channels.addAll(channelStrings); Future.delayed(Duration.zero, () => _createSocket()); int id = DateTime.now().microsecondsSinceEpoch; RealtimeSubscription subscription = RealtimeSubscription( controller: controller, - channels: channels, + channels: channelStrings, close: () async { _subscriptions.remove(id); controller.close(); - _cleanup(channels); + _cleanup(channelStrings); if (_channels.isNotEmpty) { await Future.delayed(Duration.zero, () => _createSocket()); diff --git a/pubspec.yaml b/pubspec.yaml index 84860326..0da16c31 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: appwrite -version: 20.3.3 +version: 21.0.0 description: Appwrite is an open-source self-hosted backend server that abstracts and simplifies 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/channel_test.dart b/test/channel_test.dart new file mode 100644 index 00000000..0a012f67 --- /dev/null +++ b/test/channel_test.dart @@ -0,0 +1,125 @@ +import 'package:appwrite/appwrite.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('database()', () { + test('returns database channel with defaults', () { + expect(Channel.database().collection().document().toString(), + 'databases.*.collections.*.documents.*'); + }); + + test('returns database channel with specific IDs', () { + expect( + Channel.database('db1') + .collection('col1') + .document('doc1') + .toString(), + 'databases.db1.collections.col1.documents.doc1'); + }); + + test('returns database channel with action', () { + expect( + Channel.database('db1') + .collection('col1') + .document('doc1') + .create() + .toString(), + 'databases.db1.collections.col1.documents.doc1.create'); + }); + }); + + group('tablesdb()', () { + test('returns tablesdb channel with defaults', () { + expect(Channel.tablesdb().table().row().toString(), + 'tablesdb.*.tables.*.rows.*'); + }); + + test('returns tablesdb channel with specific IDs', () { + expect(Channel.tablesdb('db1').table('table1').row('row1').toString(), + 'tablesdb.db1.tables.table1.rows.row1'); + }); + + test('returns tablesdb channel with action', () { + expect( + Channel.tablesdb('db1') + .table('table1') + .row('row1') + .update() + .toString(), + 'tablesdb.db1.tables.table1.rows.row1.update'); + }); + }); + + group('account()', () { + test('returns account channel with default', () { + expect(Channel.account(), 'account.*'); + }); + + test('returns account channel with specific user ID', () { + expect(Channel.account('user123'), 'account.user123'); + }); + }); + + group('bucket()', () { + test('returns buckets channel with defaults', () { + expect(Channel.bucket().file().toString(), 'buckets.*.files.*'); + }); + + test('returns buckets channel with specific IDs', () { + expect(Channel.bucket('bucket1').file('file1').toString(), + 'buckets.bucket1.files.file1'); + }); + + test('returns buckets channel with action', () { + expect(Channel.bucket('bucket1').file('file1').delete().toString(), + 'buckets.bucket1.files.file1.delete'); + }); + }); + + group('functions()', () { + test('returns functions channel with defaults', () { + expect(Channel.function().execution().toString(), + 'functions.*.executions.*'); + }); + + test('returns functions channel with specific IDs', () { + expect(Channel.function('func1').execution('exec1').toString(), + 'functions.func1.executions.exec1'); + }); + + test('returns functions channel with action', () { + expect(Channel.function('func1').execution('exec1').create().toString(), + 'functions.func1.executions.exec1.create'); + }); + }); + + group('teams()', () { + test('returns teams channel with default', () { + expect(Channel.team().toString(), 'teams.*'); + }); + + test('returns teams channel with specific team ID', () { + expect(Channel.team('team1').toString(), 'teams.team1'); + }); + + test('returns teams channel with action', () { + expect(Channel.team('team1').create().toString(), 'teams.team1.create'); + }); + }); + + group('memberships()', () { + test('returns memberships channel with default', () { + expect(Channel.membership().toString(), 'memberships.*'); + }); + + test('returns memberships channel with specific membership ID', () { + expect(Channel.membership('membership1').toString(), + 'memberships.membership1'); + }); + + test('returns memberships channel with action', () { + expect(Channel.membership('membership1').update().toString(), + 'memberships.membership1.update'); + }); + }); +} diff --git a/test/query_test.dart b/test/query_test.dart index 0234a78a..db7b9214 100644 --- a/test/query_test.dart +++ b/test/query_test.dart @@ -272,6 +272,46 @@ void main() { expect(query['method'], 'notEndsWith'); }); + test('returns regex', () { + final query = jsonDecode(Query.regex('attr', 'pattern.*')); + expect(query['attribute'], 'attr'); + expect(query['values'], ['pattern.*']); + expect(query['method'], 'regex'); + }); + + test('returns exists', () { + final query = jsonDecode(Query.exists(['attr1', 'attr2'])); + expect(query['attribute'], null); + expect(query['values'], ['attr1', 'attr2']); + expect(query['method'], 'exists'); + }); + + test('returns notExists', () { + final query = jsonDecode(Query.notExists(['attr1', 'attr2'])); + expect(query['attribute'], null); + expect(query['values'], ['attr1', 'attr2']); + expect(query['method'], 'notExists'); + }); + + test('returns elemMatch', () { + final inner1 = Query.equal('name', 'Alice'); + final inner2 = Query.greaterThan('age', 18); + + final query = jsonDecode(Query.elemMatch('friends', [inner1, inner2])); + + expect(query['attribute'], 'friends'); + expect(query['method'], 'elemMatch'); + expect(query['values'].length, 2); + + expect(query['values'][0]['method'], 'equal'); + expect(query['values'][0]['attribute'], 'name'); + expect(query['values'][0]['values'], ['Alice']); + + expect(query['values'][1]['method'], 'greaterThan'); + expect(query['values'][1]['attribute'], 'age'); + expect(query['values'][1]['values'], [18]); + }); + test('returns createdBefore', () { final query = jsonDecode(Query.createdBefore('2023-01-01')); expect(query['attribute'], '\$createdAt'); diff --git a/test/services/storage_test.dart b/test/services/storage_test.dart index 19b0d334..a7cb9bd8 100644 --- a/test/services/storage_test.dart +++ b/test/services/storage_test.dart @@ -86,6 +86,8 @@ void main() { 'sizeOriginal': 17890, 'chunksTotal': 17890, 'chunksUploaded': 17890, + 'encryption': true, + 'compression': 'gzip', }; when(client.chunkedUpload( @@ -117,6 +119,8 @@ void main() { 'sizeOriginal': 17890, 'chunksTotal': 17890, 'chunksUploaded': 17890, + 'encryption': true, + 'compression': 'gzip', }; when(client.call( @@ -143,6 +147,8 @@ void main() { 'sizeOriginal': 17890, 'chunksTotal': 17890, 'chunksUploaded': 17890, + 'encryption': true, + 'compression': 'gzip', }; when(client.call( diff --git a/test/services/teams_test.dart b/test/services/teams_test.dart index 9a2eb930..0b934243 100644 --- a/test/services/teams_test.dart +++ b/test/services/teams_test.dart @@ -184,7 +184,7 @@ void main() { final response = await teams.createMembership( teamId: '', - roles: [], + roles: [enums.Roles.admin], ); expect(response, isA()); }); @@ -241,7 +241,7 @@ void main() { final response = await teams.updateMembership( teamId: '', membershipId: '', - roles: [], + roles: [enums.Roles.admin], ); expect(response, isA()); }); diff --git a/test/src/models/file_test.dart b/test/src/models/file_test.dart index e6d6dae5..c01df19b 100644 --- a/test/src/models/file_test.dart +++ b/test/src/models/file_test.dart @@ -16,6 +16,8 @@ void main() { sizeOriginal: 17890, chunksTotal: 17890, chunksUploaded: 17890, + encryption: true, + compression: 'gzip', ); final map = model.toMap(); @@ -32,6 +34,8 @@ void main() { expect(result.sizeOriginal, 17890); expect(result.chunksTotal, 17890); expect(result.chunksUploaded, 17890); + expect(result.encryption, true); + expect(result.compression, 'gzip'); }); }); }