diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index 5127d8be..0e4bf293 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -13,13 +13,13 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: true ref: ${{ github.event.pull_request.head.ref }} - name: Install Flutter - uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2 + uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2.23.0 with: channel: stable @@ -33,7 +33,7 @@ jobs: run: git config --global --add safe.directory /__w/sdk-for-flutter/sdk-for-flutter # required to fix dubious ownership - name: Add & Commit - uses: EndBug/add-and-commit@a94899bca583c204427a224a7af87c02f9b325d5 # v9.1.4 + uses: EndBug/add-and-commit@290ea2c423ad77ca9c62ae0f5b224379612c0321 # v10.0.0 with: add: '["lib", "test"]' diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 045dcbaf..aefb3e10 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -11,9 +11,9 @@ jobs: id-token: write runs-on: ubuntu-latest steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Install Flutter - uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2 + uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2.23.0 with: channel: stable - name: Install dependencies diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 56f26a0d..a84bb7b9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,9 +6,9 @@ jobs: test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Install Flutter - uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2 + uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2.23.0 with: channel: stable - run: flutter pub get diff --git a/CHANGELOG.md b/CHANGELOG.md index f492c6b8..99ca59f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Change Log +## 25.4.0 + +* Added: `Organization` service for managing app installations +* Added: app installation management methods to the `Teams` service +* Added: account OAuth2 consent methods `listConsents`, `getConsent`, `deleteConsent` +* Added: account consent token methods `listConsentTokens`, `getConsentToken`, `deleteConsentToken` +* Added: `folder` parameter to `storage.createFile`, and `folder` and `key` to `File` +* Fixed: Null optional parameters are no longer sent in request bodies +* Fixed: Binary endpoints now authenticate with headers, not query parameters + ## 25.3.0 * Added: `Client.setBearer()` method for OAuth access token authentication diff --git a/README.md b/README.md index 804fca53..d65b5309 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Add this to your package's `pubspec.yaml` file: ```yml dependencies: - appwrite: ^25.3.0 + appwrite: ^25.4.0 ``` You can install packages from the command line: diff --git a/docs/examples/account/delete-consent-token.md b/docs/examples/account/delete-consent-token.md new file mode 100644 index 00000000..9c877479 --- /dev/null +++ b/docs/examples/account/delete-consent-token.md @@ -0,0 +1,14 @@ +```dart +import 'package:appwrite/appwrite.dart'; + +Client client = Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject(''); // Your project ID + +Account account = Account(client); + +await account.deleteConsentToken( + consentId: '', + tokenId: '', +); +``` diff --git a/docs/examples/account/delete-consent.md b/docs/examples/account/delete-consent.md new file mode 100644 index 00000000..78799117 --- /dev/null +++ b/docs/examples/account/delete-consent.md @@ -0,0 +1,13 @@ +```dart +import 'package:appwrite/appwrite.dart'; + +Client client = Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject(''); // Your project ID + +Account account = Account(client); + +await account.deleteConsent( + consentId: '', +); +``` diff --git a/docs/examples/account/get-consent-token.md b/docs/examples/account/get-consent-token.md new file mode 100644 index 00000000..67a0d842 --- /dev/null +++ b/docs/examples/account/get-consent-token.md @@ -0,0 +1,14 @@ +```dart +import 'package:appwrite/appwrite.dart'; + +Client client = Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject(''); // Your project ID + +Account account = Account(client); + +Oauth2ConsentToken result = await account.getConsentToken( + consentId: '', + tokenId: '', +); +``` diff --git a/docs/examples/account/get-consent.md b/docs/examples/account/get-consent.md new file mode 100644 index 00000000..aff62725 --- /dev/null +++ b/docs/examples/account/get-consent.md @@ -0,0 +1,13 @@ +```dart +import 'package:appwrite/appwrite.dart'; + +Client client = Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject(''); // Your project ID + +Account account = Account(client); + +Oauth2Consent result = await account.getConsent( + consentId: '', +); +``` diff --git a/docs/examples/account/list-consent-tokens.md b/docs/examples/account/list-consent-tokens.md new file mode 100644 index 00000000..835c7775 --- /dev/null +++ b/docs/examples/account/list-consent-tokens.md @@ -0,0 +1,15 @@ +```dart +import 'package:appwrite/appwrite.dart'; + +Client client = Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject(''); // Your project ID + +Account account = Account(client); + +Oauth2ConsentTokenList result = await account.listConsentTokens( + consentId: '', + queries: [], // optional + total: false, // optional +); +``` diff --git a/docs/examples/account/list-consents.md b/docs/examples/account/list-consents.md new file mode 100644 index 00000000..d39f0547 --- /dev/null +++ b/docs/examples/account/list-consents.md @@ -0,0 +1,14 @@ +```dart +import 'package:appwrite/appwrite.dart'; + +Client client = Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject(''); // Your project ID + +Account account = Account(client); + +Oauth2ConsentList result = await account.listConsents( + queries: [], // optional + total: false, // optional +); +``` diff --git a/docs/examples/organization/create-installation.md b/docs/examples/organization/create-installation.md new file mode 100644 index 00000000..d28dacfc --- /dev/null +++ b/docs/examples/organization/create-installation.md @@ -0,0 +1,14 @@ +```dart +import 'package:appwrite/appwrite.dart'; + +Client client = Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject(''); // Your project ID + +Organization organization = Organization(client); + +AppInstallation result = await organization.createInstallation( + appId: '', + authorizationDetails: '', // optional +); +``` diff --git a/docs/examples/organization/delete-installation.md b/docs/examples/organization/delete-installation.md new file mode 100644 index 00000000..3142e245 --- /dev/null +++ b/docs/examples/organization/delete-installation.md @@ -0,0 +1,13 @@ +```dart +import 'package:appwrite/appwrite.dart'; + +Client client = Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject(''); // Your project ID + +Organization organization = Organization(client); + +await organization.deleteInstallation( + installationId: '', +); +``` diff --git a/docs/examples/organization/get-installation.md b/docs/examples/organization/get-installation.md new file mode 100644 index 00000000..dd2d7ac7 --- /dev/null +++ b/docs/examples/organization/get-installation.md @@ -0,0 +1,13 @@ +```dart +import 'package:appwrite/appwrite.dart'; + +Client client = Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject(''); // Your project ID + +Organization organization = Organization(client); + +AppInstallation result = await organization.getInstallation( + installationId: '', +); +``` diff --git a/docs/examples/organization/list-installations.md b/docs/examples/organization/list-installations.md new file mode 100644 index 00000000..bdecb108 --- /dev/null +++ b/docs/examples/organization/list-installations.md @@ -0,0 +1,14 @@ +```dart +import 'package:appwrite/appwrite.dart'; + +Client client = Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject(''); // Your project ID + +Organization organization = Organization(client); + +AppInstallationList result = await organization.listInstallations( + queries: [], // optional + total: false, // optional +); +``` diff --git a/docs/examples/organization/update-installation.md b/docs/examples/organization/update-installation.md new file mode 100644 index 00000000..fa834d49 --- /dev/null +++ b/docs/examples/organization/update-installation.md @@ -0,0 +1,14 @@ +```dart +import 'package:appwrite/appwrite.dart'; + +Client client = Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject(''); // Your project ID + +Organization organization = Organization(client); + +AppInstallation result = await organization.updateInstallation( + installationId: '', + authorizationDetails: '', // optional +); +``` diff --git a/docs/examples/storage/create-file.md b/docs/examples/storage/create-file.md index 8f904801..007abee4 100644 --- a/docs/examples/storage/create-file.md +++ b/docs/examples/storage/create-file.md @@ -15,5 +15,6 @@ File result = await storage.createFile( fileId: '', file: InputFile(path: './path-to-files/image.jpg', filename: 'image.jpg'), permissions: [Permission.read(Role.any())], // optional + folder: '', // optional ); ``` diff --git a/docs/examples/teams/create-installation.md b/docs/examples/teams/create-installation.md new file mode 100644 index 00000000..4ad63b8d --- /dev/null +++ b/docs/examples/teams/create-installation.md @@ -0,0 +1,15 @@ +```dart +import 'package:appwrite/appwrite.dart'; + +Client client = Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject(''); // Your project ID + +Teams teams = Teams(client); + +AppInstallation result = await teams.createInstallation( + teamId: '', + appId: '', + authorizationDetails: '', // optional +); +``` diff --git a/docs/examples/teams/delete-installation.md b/docs/examples/teams/delete-installation.md new file mode 100644 index 00000000..4c872cec --- /dev/null +++ b/docs/examples/teams/delete-installation.md @@ -0,0 +1,14 @@ +```dart +import 'package:appwrite/appwrite.dart'; + +Client client = Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject(''); // Your project ID + +Teams teams = Teams(client); + +await teams.deleteInstallation( + teamId: '', + installationId: '', +); +``` diff --git a/docs/examples/teams/get-installation.md b/docs/examples/teams/get-installation.md new file mode 100644 index 00000000..eec87662 --- /dev/null +++ b/docs/examples/teams/get-installation.md @@ -0,0 +1,14 @@ +```dart +import 'package:appwrite/appwrite.dart'; + +Client client = Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject(''); // Your project ID + +Teams teams = Teams(client); + +AppInstallation result = await teams.getInstallation( + teamId: '', + installationId: '', +); +``` diff --git a/docs/examples/teams/list-installations.md b/docs/examples/teams/list-installations.md new file mode 100644 index 00000000..9c3aa852 --- /dev/null +++ b/docs/examples/teams/list-installations.md @@ -0,0 +1,15 @@ +```dart +import 'package:appwrite/appwrite.dart'; + +Client client = Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject(''); // Your project ID + +Teams teams = Teams(client); + +AppInstallationList result = await teams.listInstallations( + teamId: '', + queries: [], // optional + total: false, // optional +); +``` diff --git a/docs/examples/teams/update-installation.md b/docs/examples/teams/update-installation.md new file mode 100644 index 00000000..e9e8ba4b --- /dev/null +++ b/docs/examples/teams/update-installation.md @@ -0,0 +1,15 @@ +```dart +import 'package:appwrite/appwrite.dart'; + +Client client = Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject(''); // Your project ID + +Teams teams = Teams(client); + +AppInstallation result = await teams.updateInstallation( + teamId: '', + installationId: '', + authorizationDetails: '', // optional +); +``` diff --git a/lib/appwrite.dart b/lib/appwrite.dart index ae51be5e..f55bd86a 100644 --- a/lib/appwrite.dart +++ b/lib/appwrite.dart @@ -39,6 +39,7 @@ part 'services/functions.dart'; part 'services/graphql.dart'; part 'services/locale.dart'; part 'services/messaging.dart'; +part 'services/organization.dart'; part 'services/presences.dart'; part 'services/storage.dart'; part 'services/tables_db.dart'; diff --git a/lib/models.dart b/lib/models.dart index fb5d3f8d..d6ee0466 100644 --- a/lib/models.dart +++ b/lib/models.dart @@ -57,3 +57,9 @@ part 'src/models/mfa_factors.dart'; part 'src/models/transaction.dart'; part 'src/models/subscriber.dart'; part 'src/models/target.dart'; +part 'src/models/app_installation.dart'; +part 'src/models/oauth2_consent.dart'; +part 'src/models/oauth2_consent_token.dart'; +part 'src/models/oauth2_consent_list.dart'; +part 'src/models/oauth2_consent_token_list.dart'; +part 'src/models/app_installation_list.dart'; diff --git a/lib/services/account.dart b/lib/services/account.dart index 48b60041..3b8fbded 100644 --- a/lib/services/account.dart +++ b/lib/services/account.dart @@ -55,6 +55,136 @@ class Account extends Service { return models.User.fromMap(res.data); } + /// Get a list of the OAuth2 consents the current user has given to third-party + /// apps. + Future listConsents( + {List? queries, bool? total}) async { + final String apiPath = '/account/consents'; + + final Map apiParams = { + if (queries != null) 'queries': queries, + if (total != null) 'total': total, + }; + + final Map apiHeaders = { + 'X-Appwrite-Project': client.config['project'] ?? '', + 'accept': 'application/json', + }; + + final res = await client.call(HttpMethod.get, + path: apiPath, params: apiParams, headers: apiHeaders); + + return models.Oauth2ConsentList.fromMap(res.data); + } + + /// Get an OAuth2 consent the current user has given to a third-party app by + /// its unique ID. + Future getConsent({required String consentId}) async { + final String apiPath = + '/account/consents/{consentId}'.replaceAll('{consentId}', consentId); + + final Map apiParams = {}; + + final Map apiHeaders = { + 'X-Appwrite-Project': client.config['project'] ?? '', + 'accept': 'application/json', + }; + + final res = await client.call(HttpMethod.get, + path: apiPath, params: apiParams, headers: apiHeaders); + + return models.Oauth2Consent.fromMap(res.data); + } + + /// Delete an OAuth2 consent by its unique ID. All token families issued under + /// the consent are revoked, and the app must ask for consent again to regain + /// access. + Future deleteConsent({required String consentId}) async { + final String apiPath = + '/account/consents/{consentId}'.replaceAll('{consentId}', consentId); + + final Map apiParams = {}; + + final Map apiHeaders = { + 'X-Appwrite-Project': client.config['project'] ?? '', + 'content-type': 'application/json', + 'accept': 'application/json', + }; + + final res = await client.call(HttpMethod.delete, + path: apiPath, params: apiParams, headers: apiHeaders); + + return res.data; + } + + /// Get a list of the token families issued under an OAuth2 consent. Each entry + /// represents one authorized device or session; the token secrets themselves + /// are never returned. + Future listConsentTokens( + {required String consentId, List? queries, bool? total}) async { + final String apiPath = '/account/consents/{consentId}/tokens' + .replaceAll('{consentId}', consentId); + + final Map apiParams = { + if (queries != null) 'queries': queries, + if (total != null) 'total': total, + }; + + final Map apiHeaders = { + 'X-Appwrite-Project': client.config['project'] ?? '', + 'accept': 'application/json', + }; + + final res = await client.call(HttpMethod.get, + path: apiPath, params: apiParams, headers: apiHeaders); + + return models.Oauth2ConsentTokenList.fromMap(res.data); + } + + /// Get a token family issued under an OAuth2 consent by its unique ID. The + /// token secrets themselves are never returned. + Future getConsentToken( + {required String consentId, required String tokenId}) async { + final String apiPath = '/account/consents/{consentId}/tokens/{tokenId}' + .replaceAll('{consentId}', consentId) + .replaceAll('{tokenId}', tokenId); + + final Map apiParams = {}; + + final Map apiHeaders = { + 'X-Appwrite-Project': client.config['project'] ?? '', + 'accept': 'application/json', + }; + + final res = await client.call(HttpMethod.get, + path: apiPath, params: apiParams, headers: apiHeaders); + + return models.Oauth2ConsentToken.fromMap(res.data); + } + + /// Delete a token family issued under an OAuth2 consent by its unique ID. The + /// access and refresh tokens of the family stop working immediately; other + /// token families and the consent itself are unaffected. + Future deleteConsentToken( + {required String consentId, required String tokenId}) async { + final String apiPath = '/account/consents/{consentId}/tokens/{tokenId}' + .replaceAll('{consentId}', consentId) + .replaceAll('{tokenId}', tokenId); + + final Map apiParams = {}; + + final Map apiHeaders = { + 'X-Appwrite-Project': client.config['project'] ?? '', + 'content-type': 'application/json', + 'accept': 'application/json', + }; + + final res = await client.call(HttpMethod.delete, + path: apiPath, params: apiParams, headers: apiHeaders); + + return res.data; + } + /// Update currently logged in user account email address. After changing user /// address, the user confirmation status will get reset. A new confirmation /// email is not sent automatically however you can use the send confirmation diff --git a/lib/services/avatars.dart b/lib/services/avatars.dart index cdd93e6e..2fe54e87 100644 --- a/lib/services/avatars.dart +++ b/lib/services/avatars.dart @@ -24,16 +24,22 @@ class Avatars extends Service { final String apiPath = '/avatars/browsers/{code}'.replaceAll('{code}', code.value); - final Map params = { + final Map apiParams = { if (width != null) 'width': width, if (height != null) 'height': height, if (quality != null) 'quality': quality, - 'project': client.config['project'], - 'impersonateuserid': client.config['impersonateuserid'], + }; + + final Map apiHeaders = { + 'X-Appwrite-Project': client.config['project'] ?? '', + 'accept': 'image/png', }; final res = await client.call(HttpMethod.get, - path: apiPath, params: params, responseType: ResponseType.bytes); + path: apiPath, + params: apiParams, + headers: apiHeaders, + responseType: ResponseType.bytes); return res.data; } @@ -54,16 +60,22 @@ class Avatars extends Service { final String apiPath = '/avatars/credit-cards/{code}'.replaceAll('{code}', code.value); - final Map params = { + final Map apiParams = { if (width != null) 'width': width, if (height != null) 'height': height, if (quality != null) 'quality': quality, - 'project': client.config['project'], - 'impersonateuserid': client.config['impersonateuserid'], + }; + + final Map apiHeaders = { + 'X-Appwrite-Project': client.config['project'] ?? '', + 'accept': 'image/png', }; final res = await client.call(HttpMethod.get, - path: apiPath, params: params, responseType: ResponseType.bytes); + path: apiPath, + params: apiParams, + headers: apiHeaders, + responseType: ResponseType.bytes); return res.data; } @@ -74,14 +86,20 @@ class Avatars extends Service { Future getFavicon({required String url}) async { final String apiPath = '/avatars/favicon'; - final Map params = { + final Map apiParams = { 'url': url, - 'project': client.config['project'], - 'impersonateuserid': client.config['impersonateuserid'], + }; + + final Map apiHeaders = { + 'X-Appwrite-Project': client.config['project'] ?? '', + 'accept': 'image/*', }; final res = await client.call(HttpMethod.get, - path: apiPath, params: params, responseType: ResponseType.bytes); + path: apiPath, + params: apiParams, + headers: apiHeaders, + responseType: ResponseType.bytes); return res.data; } @@ -100,16 +118,22 @@ class Avatars extends Service { final String apiPath = '/avatars/flags/{code}'.replaceAll('{code}', code.value); - final Map params = { + final Map apiParams = { if (width != null) 'width': width, if (height != null) 'height': height, if (quality != null) 'quality': quality, - 'project': client.config['project'], - 'impersonateuserid': client.config['impersonateuserid'], + }; + + final Map apiHeaders = { + 'X-Appwrite-Project': client.config['project'] ?? '', + 'accept': 'image/png', }; final res = await client.call(HttpMethod.get, - path: apiPath, params: params, responseType: ResponseType.bytes); + path: apiPath, + params: apiParams, + headers: apiHeaders, + responseType: ResponseType.bytes); return res.data; } @@ -128,16 +152,22 @@ class Avatars extends Service { {required String url, int? width, int? height}) async { final String apiPath = '/avatars/image'; - final Map params = { + final Map apiParams = { 'url': url, if (width != null) 'width': width, if (height != null) 'height': height, - 'project': client.config['project'], - 'impersonateuserid': client.config['impersonateuserid'], + }; + + final Map apiHeaders = { + 'X-Appwrite-Project': client.config['project'] ?? '', + 'accept': 'image/*', }; final res = await client.call(HttpMethod.get, - path: apiPath, params: params, responseType: ResponseType.bytes); + path: apiPath, + params: apiParams, + headers: apiHeaders, + responseType: ResponseType.bytes); return res.data; } @@ -161,17 +191,23 @@ class Avatars extends Service { {String? name, int? width, int? height, String? background}) async { final String apiPath = '/avatars/initials'; - final Map params = { + final Map apiParams = { if (name != null) 'name': name, if (width != null) 'width': width, if (height != null) 'height': height, if (background != null) 'background': background, - 'project': client.config['project'], - 'impersonateuserid': client.config['impersonateuserid'], + }; + + final Map apiHeaders = { + 'X-Appwrite-Project': client.config['project'] ?? '', + 'accept': 'image/png', }; final res = await client.call(HttpMethod.get, - path: apiPath, params: params, responseType: ResponseType.bytes); + path: apiPath, + params: apiParams, + headers: apiHeaders, + responseType: ResponseType.bytes); return res.data; } @@ -182,17 +218,23 @@ class Avatars extends Service { {required String text, int? size, int? margin, bool? download}) async { final String apiPath = '/avatars/qr'; - final Map params = { + final Map apiParams = { 'text': text, if (size != null) 'size': size, if (margin != null) 'margin': margin, if (download != null) 'download': download, - 'project': client.config['project'], - 'impersonateuserid': client.config['impersonateuserid'], + }; + + final Map apiHeaders = { + 'X-Appwrite-Project': client.config['project'] ?? '', + 'accept': 'image/png', }; final res = await client.call(HttpMethod.get, - path: apiPath, params: params, responseType: ResponseType.bytes); + path: apiPath, + params: apiParams, + headers: apiHeaders, + responseType: ResponseType.bytes); return res.data; } @@ -229,7 +271,7 @@ class Avatars extends Service { enums.ImageFormat? output}) async { final String apiPath = '/avatars/screenshots'; - final Map params = { + final Map apiParams = { 'url': url, if (headers != null) 'headers': headers, if (viewportWidth != null) 'viewportWidth': viewportWidth, @@ -251,12 +293,18 @@ class Avatars extends Service { if (height != null) 'height': height, if (quality != null) 'quality': quality, if (output != null) 'output': output.value, - 'project': client.config['project'], - 'impersonateuserid': client.config['impersonateuserid'], + }; + + final Map apiHeaders = { + 'X-Appwrite-Project': client.config['project'] ?? '', + 'accept': 'image/png', }; final res = await client.call(HttpMethod.get, - path: apiPath, params: params, responseType: ResponseType.bytes); + path: apiPath, + params: apiParams, + headers: apiHeaders, + responseType: ResponseType.bytes); return res.data; } } diff --git a/lib/services/databases.dart b/lib/services/databases.dart index 0ff3fb72..d298cec0 100644 --- a/lib/services/databases.dart +++ b/lib/services/databases.dart @@ -195,8 +195,8 @@ class Databases extends Service { final Map apiParams = { 'documentId': documentId, 'data': data, - 'permissions': permissions, - 'transactionId': transactionId, + if (permissions != null) 'permissions': permissions, + if (transactionId != null) 'transactionId': transactionId, }; final Map apiHeaders = { @@ -264,8 +264,8 @@ class Databases extends Service { final Map apiParams = { if (data != null) 'data': data, - 'permissions': permissions, - 'transactionId': transactionId, + if (permissions != null) 'permissions': permissions, + if (transactionId != null) 'transactionId': transactionId, }; final Map apiHeaders = { @@ -299,8 +299,8 @@ class Databases extends Service { final Map apiParams = { if (data != null) 'data': data, - 'permissions': permissions, - 'transactionId': transactionId, + if (permissions != null) 'permissions': permissions, + if (transactionId != null) 'transactionId': transactionId, }; final Map apiHeaders = { @@ -364,8 +364,8 @@ class Databases extends Service { final Map apiParams = { if (value != null) 'value': value, - 'min': min, - 'transactionId': transactionId, + if (min != null) 'min': min, + if (transactionId != null) 'transactionId': transactionId, }; final Map apiHeaders = { @@ -400,8 +400,8 @@ class Databases extends Service { final Map apiParams = { if (value != null) 'value': value, - 'max': max, - 'transactionId': transactionId, + if (max != null) 'max': max, + if (transactionId != null) 'transactionId': transactionId, }; final Map apiHeaders = { diff --git a/lib/services/functions.dart b/lib/services/functions.dart index ed97921b..6cde02ed 100644 --- a/lib/services/functions.dart +++ b/lib/services/functions.dart @@ -50,7 +50,7 @@ class Functions extends Service { if (path != null) 'path': path, if (method != null) 'method': method.value, if (headers != null) 'headers': headers, - 'scheduledAt': scheduledAt, + if (scheduledAt != null) 'scheduledAt': scheduledAt, }; final Map apiHeaders = { diff --git a/lib/services/organization.dart b/lib/services/organization.dart new file mode 100644 index 00000000..68275296 --- /dev/null +++ b/lib/services/organization.dart @@ -0,0 +1,121 @@ +part of '../appwrite.dart'; + +/// The Organization service allows you to manage organization-level projects. +class Organization extends Service { + /// Initializes a [Organization] service + Organization(super.client); + + /// List app installations on the organization. Any organization member can + /// read installations. + Future listInstallations( + {List? queries, bool? total}) async { + final String apiPath = '/organization/installations'; + + final Map apiParams = { + if (queries != null) 'queries': queries, + if (total != null) 'total': total, + }; + + final Map apiHeaders = { + 'X-Appwrite-Project': client.config['project'] ?? '', + 'accept': 'application/json', + }; + + final res = await client.call(HttpMethod.get, + path: apiPath, params: apiParams, headers: apiHeaders); + + return models.AppInstallationList.fromMap(res.data); + } + + /// Install an app on the organization. Only organization members with the + /// owner role can install apps. The installation is granted the scopes the app + /// currently requests. + Future createInstallation( + {required String appId, String? authorizationDetails}) async { + final String apiPath = '/organization/installations'; + + final Map apiParams = { + 'appId': appId, + if (authorizationDetails != null) + 'authorizationDetails': authorizationDetails, + }; + + final Map apiHeaders = { + 'X-Appwrite-Project': client.config['project'] ?? '', + 'content-type': 'application/json', + 'accept': 'application/json', + }; + + final res = await client.call(HttpMethod.post, + path: apiPath, params: apiParams, headers: apiHeaders); + + return models.AppInstallation.fromMap(res.data); + } + + /// Get an app installation on the organization by its unique ID. Any + /// organization member can read installations. + Future getInstallation( + {required String installationId}) async { + final String apiPath = '/organization/installations/{installationId}' + .replaceAll('{installationId}', installationId); + + final Map apiParams = {}; + + final Map apiHeaders = { + 'X-Appwrite-Project': client.config['project'] ?? '', + 'accept': 'application/json', + }; + + final res = await client.call(HttpMethod.get, + path: apiPath, params: apiParams, headers: apiHeaders); + + return models.AppInstallation.fromMap(res.data); + } + + /// Update an app installation on the organization. Only organization members + /// with the owner role can update installations. The installation's granted + /// scopes are refreshed to the scopes the app currently requests; previously + /// issued installation access tokens are revoked. + Future updateInstallation( + {required String installationId, String? authorizationDetails}) async { + final String apiPath = '/organization/installations/{installationId}' + .replaceAll('{installationId}', installationId); + + final Map apiParams = { + if (authorizationDetails != null) + 'authorizationDetails': authorizationDetails, + }; + + final Map apiHeaders = { + 'X-Appwrite-Project': client.config['project'] ?? '', + 'content-type': 'application/json', + 'accept': 'application/json', + }; + + final res = await client.call(HttpMethod.put, + path: apiPath, params: apiParams, headers: apiHeaders); + + return models.AppInstallation.fromMap(res.data); + } + + /// Uninstall an app from the organization by its installation ID. Only + /// organization members with the owner role can remove installations. + /// Previously issued installation access tokens are revoked. + Future deleteInstallation({required String installationId}) async { + final String apiPath = '/organization/installations/{installationId}' + .replaceAll('{installationId}', installationId); + + final Map apiParams = {}; + + final Map apiHeaders = { + 'X-Appwrite-Project': client.config['project'] ?? '', + 'content-type': 'application/json', + 'accept': 'application/json', + }; + + final res = await client.call(HttpMethod.delete, + path: apiPath, params: apiParams, headers: apiHeaders); + + return res.data; + } +} diff --git a/lib/services/presences.dart b/lib/services/presences.dart index 1c9b651b..fd5d6be2 100644 --- a/lib/services/presences.dart +++ b/lib/services/presences.dart @@ -1,5 +1,7 @@ part of '../appwrite.dart'; +/// The Presences service allows you to track and manage real-time user +/// presence in your project. class Presences extends Service { /// Initializes a [Presences] service Presences(super.client); diff --git a/lib/services/storage.dart b/lib/services/storage.dart index cdb626de..d261ef17 100644 --- a/lib/services/storage.dart +++ b/lib/services/storage.dart @@ -55,6 +55,7 @@ class Storage extends Service { required String fileId, required InputFile file, List? permissions, + String? folder, Function(UploadProgress)? onProgress}) async { final String apiPath = '/storage/buckets/{bucketId}/files'.replaceAll('{bucketId}', bucketId); @@ -63,6 +64,7 @@ class Storage extends Service { 'fileId': fileId, 'file': file, if (permissions != null) 'permissions': permissions, + if (folder != null) 'folder': folder, }; final Map apiHeaders = { @@ -120,7 +122,7 @@ class Storage extends Service { final Map apiParams = { if (name != null) 'name': name, - 'permissions': permissions, + if (permissions != null) 'permissions': permissions, }; final Map apiHeaders = { @@ -164,14 +166,20 @@ class Storage extends Service { .replaceAll('{bucketId}', bucketId) .replaceAll('{fileId}', fileId); - final Map params = { + final Map apiParams = { if (token != null) 'token': token, - 'project': client.config['project'], - 'impersonateuserid': client.config['impersonateuserid'], + }; + + final Map apiHeaders = { + 'X-Appwrite-Project': client.config['project'] ?? '', + 'accept': '*/*', }; final res = await client.call(HttpMethod.get, - path: apiPath, params: params, responseType: ResponseType.bytes); + path: apiPath, + params: apiParams, + headers: apiHeaders, + responseType: ResponseType.bytes); return res.data; } @@ -199,7 +207,7 @@ class Storage extends Service { .replaceAll('{bucketId}', bucketId) .replaceAll('{fileId}', fileId); - final Map params = { + final Map apiParams = { if (width != null) 'width': width, if (height != null) 'height': height, if (gravity != null) 'gravity': gravity.value, @@ -212,12 +220,18 @@ class Storage extends Service { if (background != null) 'background': background, if (output != null) 'output': output.value, if (token != null) 'token': token, - 'project': client.config['project'], - 'impersonateuserid': client.config['impersonateuserid'], + }; + + final Map apiHeaders = { + 'X-Appwrite-Project': client.config['project'] ?? '', + 'accept': 'image/*', }; final res = await client.call(HttpMethod.get, - path: apiPath, params: params, responseType: ResponseType.bytes); + path: apiPath, + params: apiParams, + headers: apiHeaders, + responseType: ResponseType.bytes); return res.data; } @@ -230,14 +244,20 @@ class Storage extends Service { .replaceAll('{bucketId}', bucketId) .replaceAll('{fileId}', fileId); - final Map params = { + final Map apiParams = { if (token != null) 'token': token, - 'project': client.config['project'], - 'impersonateuserid': client.config['impersonateuserid'], + }; + + final Map apiHeaders = { + 'X-Appwrite-Project': client.config['project'] ?? '', + 'accept': '*/*', }; final res = await client.call(HttpMethod.get, - path: apiPath, params: params, responseType: ResponseType.bytes); + path: apiPath, + params: apiParams, + headers: apiHeaders, + responseType: ResponseType.bytes); return res.data; } } diff --git a/lib/services/tables_db.dart b/lib/services/tables_db.dart index 8b6284b3..45c239e6 100644 --- a/lib/services/tables_db.dart +++ b/lib/services/tables_db.dart @@ -1,5 +1,7 @@ part of '../appwrite.dart'; +/// The TablesDB service allows you to create structured tables of columns, +/// query and filter lists of rows class TablesDB extends Service { /// Initializes a [TablesDB] service TablesDB(super.client); @@ -175,8 +177,8 @@ class TablesDB extends Service { final Map apiParams = { 'rowId': rowId, 'data': data, - 'permissions': permissions, - 'transactionId': transactionId, + if (permissions != null) 'permissions': permissions, + if (transactionId != null) 'transactionId': transactionId, }; final Map apiHeaders = { @@ -240,8 +242,8 @@ class TablesDB extends Service { final Map apiParams = { if (data != null) 'data': data, - 'permissions': permissions, - 'transactionId': transactionId, + if (permissions != null) 'permissions': permissions, + if (transactionId != null) 'transactionId': transactionId, }; final Map apiHeaders = { @@ -273,8 +275,8 @@ class TablesDB extends Service { final Map apiParams = { if (data != null) 'data': data, - 'permissions': permissions, - 'transactionId': transactionId, + if (permissions != null) 'permissions': permissions, + if (transactionId != null) 'transactionId': transactionId, }; final Map apiHeaders = { @@ -334,8 +336,8 @@ class TablesDB extends Service { final Map apiParams = { if (value != null) 'value': value, - 'min': min, - 'transactionId': transactionId, + if (min != null) 'min': min, + if (transactionId != null) 'transactionId': transactionId, }; final Map apiHeaders = { @@ -368,8 +370,8 @@ class TablesDB extends Service { final Map apiParams = { if (value != null) 'value': value, - 'max': max, - 'transactionId': transactionId, + if (max != null) 'max': max, + if (transactionId != null) 'transactionId': transactionId, }; final Map apiHeaders = { diff --git a/lib/services/teams.dart b/lib/services/teams.dart index 1f4022f6..c9b7b646 100644 --- a/lib/services/teams.dart +++ b/lib/services/teams.dart @@ -112,6 +112,130 @@ class Teams extends Service { return res.data; } + /// List app installations on a team. Any team member can read installations. + Future listInstallations( + {required String teamId, List? queries, bool? total}) async { + final String apiPath = + '/teams/{teamId}/installations'.replaceAll('{teamId}', teamId); + + final Map apiParams = { + if (queries != null) 'queries': queries, + if (total != null) 'total': total, + }; + + final Map apiHeaders = { + 'X-Appwrite-Project': client.config['project'] ?? '', + 'accept': 'application/json', + }; + + final res = await client.call(HttpMethod.get, + path: apiPath, params: apiParams, headers: apiHeaders); + + return models.AppInstallationList.fromMap(res.data); + } + + /// Install an app on a team. When authenticated as a user, only team members + /// with the owner role can install apps. Requests using an API key or in admin + /// mode can install apps on any team. The installation is granted the scopes + /// the app currently requests. + Future createInstallation( + {required String teamId, + required String appId, + String? authorizationDetails}) async { + final String apiPath = + '/teams/{teamId}/installations'.replaceAll('{teamId}', teamId); + + final Map apiParams = { + 'appId': appId, + if (authorizationDetails != null) + 'authorizationDetails': authorizationDetails, + }; + + final Map apiHeaders = { + 'X-Appwrite-Project': client.config['project'] ?? '', + 'content-type': 'application/json', + 'accept': 'application/json', + }; + + final res = await client.call(HttpMethod.post, + path: apiPath, params: apiParams, headers: apiHeaders); + + return models.AppInstallation.fromMap(res.data); + } + + /// Get an app installation on a team by its unique ID. Any team member can + /// read installations. + Future getInstallation( + {required String teamId, required String installationId}) async { + final String apiPath = '/teams/{teamId}/installations/{installationId}' + .replaceAll('{teamId}', teamId) + .replaceAll('{installationId}', installationId); + + final Map apiParams = {}; + + final Map apiHeaders = { + 'X-Appwrite-Project': client.config['project'] ?? '', + 'accept': 'application/json', + }; + + final res = await client.call(HttpMethod.get, + path: apiPath, params: apiParams, headers: apiHeaders); + + return models.AppInstallation.fromMap(res.data); + } + + /// Update an app installation on a team. Only team members with the owner role + /// can update installations. The installation's granted scopes are refreshed + /// to the scopes the app currently requests; previously issued installation + /// access tokens are revoked. + Future updateInstallation( + {required String teamId, + required String installationId, + String? authorizationDetails}) async { + final String apiPath = '/teams/{teamId}/installations/{installationId}' + .replaceAll('{teamId}', teamId) + .replaceAll('{installationId}', installationId); + + final Map apiParams = { + if (authorizationDetails != null) + 'authorizationDetails': authorizationDetails, + }; + + final Map apiHeaders = { + 'X-Appwrite-Project': client.config['project'] ?? '', + 'content-type': 'application/json', + 'accept': 'application/json', + }; + + final res = await client.call(HttpMethod.put, + path: apiPath, params: apiParams, headers: apiHeaders); + + return models.AppInstallation.fromMap(res.data); + } + + /// Uninstall an app from a team by its installation ID. Only team members with + /// the owner role can remove installations. Previously issued installation + /// access tokens are revoked. + Future deleteInstallation( + {required String teamId, required String installationId}) async { + final String apiPath = '/teams/{teamId}/installations/{installationId}' + .replaceAll('{teamId}', teamId) + .replaceAll('{installationId}', installationId); + + final Map apiParams = {}; + + final Map apiHeaders = { + 'X-Appwrite-Project': client.config['project'] ?? '', + 'content-type': 'application/json', + 'accept': 'application/json', + }; + + final res = await client.call(HttpMethod.delete, + path: apiPath, params: apiParams, headers: apiHeaders); + + return res.data; + } + /// Use this endpoint to list a team's members using the team's ID. All team /// members have read access to this endpoint. Hide sensitive attributes from /// the response by toggling membership privacy in the Console. diff --git a/lib/src/client_browser.dart b/lib/src/client_browser.dart index 38cee6bb..47d38062 100644 --- a/lib/src/client_browser.dart +++ b/lib/src/client_browser.dart @@ -40,8 +40,8 @@ class ClientBrowser extends ClientBase with ClientMixin { 'x-sdk-name': 'Flutter', 'x-sdk-platform': 'client', 'x-sdk-language': 'flutter', - 'x-sdk-version': '25.3.0', - 'X-Appwrite-Response-Format': '1.9.5', + 'x-sdk-version': '25.4.0', + 'X-Appwrite-Response-Format': '1.9.6', }; config = {}; diff --git a/lib/src/client_io.dart b/lib/src/client_io.dart index a4cae30f..c675f9e1 100644 --- a/lib/src/client_io.dart +++ b/lib/src/client_io.dart @@ -58,8 +58,8 @@ class ClientIO extends ClientBase with ClientMixin { 'x-sdk-name': 'Flutter', 'x-sdk-platform': 'client', 'x-sdk-language': 'flutter', - 'x-sdk-version': '25.3.0', - 'X-Appwrite-Response-Format': '1.9.5', + 'x-sdk-version': '25.4.0', + 'X-Appwrite-Response-Format': '1.9.6', }; config = {}; diff --git a/lib/src/models/app_installation.dart b/lib/src/models/app_installation.dart new file mode 100644 index 00000000..3ed121ec --- /dev/null +++ b/lib/src/models/app_installation.dart @@ -0,0 +1,78 @@ +part of '../../models.dart'; + +/// AppInstallation +class AppInstallation implements Model { + /// Installation ID. + final String $id; + + /// Installation creation time in ISO 8601 format. + final String $createdAt; + + /// Installation update time in ISO 8601 format. + final String $updatedAt; + + /// ID of the installed application. + final String appId; + + /// ID of the team the application is installed on. + final String teamId; + + /// Scopes granted to the application. Snapshot of the application's installation scopes taken when the installation was created or last updated. + final List scopes; + + /// Authorization details granted to the application. Rich authorization request (RFC 9396) style entries; the Appwrite Console stores authorized project IDs here. + final Map authorizationDetails; + + /// ID of the user who created the installation. + final String createdById; + + /// Name of the user who created the installation. + final String createdByName; + + /// Time an access token was last issued for the installation in ISO 8601 format. Null if never used. + final String? lastAccessedAt; + + AppInstallation({ + required this.$id, + required this.$createdAt, + required this.$updatedAt, + required this.appId, + required this.teamId, + required this.scopes, + required this.authorizationDetails, + required this.createdById, + required this.createdByName, + this.lastAccessedAt, + }); + + factory AppInstallation.fromMap(Map map) { + return AppInstallation( + $id: map['\$id'].toString(), + $createdAt: map['\$createdAt'].toString(), + $updatedAt: map['\$updatedAt'].toString(), + appId: map['appId'].toString(), + teamId: map['teamId'].toString(), + scopes: List.from(map['scopes'] ?? []), + authorizationDetails: map['authorizationDetails'], + createdById: map['createdById'].toString(), + createdByName: map['createdByName'].toString(), + lastAccessedAt: map['lastAccessedAt']?.toString(), + ); + } + + @override + Map toMap() { + return { + "\$id": $id, + "\$createdAt": $createdAt, + "\$updatedAt": $updatedAt, + "appId": appId, + "teamId": teamId, + "scopes": scopes, + "authorizationDetails": authorizationDetails, + "createdById": createdById, + "createdByName": createdByName, + "lastAccessedAt": lastAccessedAt, + }; + } +} diff --git a/lib/src/models/app_installation_list.dart b/lib/src/models/app_installation_list.dart new file mode 100644 index 00000000..c55f831f --- /dev/null +++ b/lib/src/models/app_installation_list.dart @@ -0,0 +1,31 @@ +part of '../../models.dart'; + +/// App installations list +class AppInstallationList implements Model { + /// Total number of installations that matched your query. + final int total; + + /// List of installations. + final List installations; + + AppInstallationList({ + required this.total, + required this.installations, + }); + + factory AppInstallationList.fromMap(Map map) { + return AppInstallationList( + total: map['total'], + installations: List.from( + map['installations'].map((p) => AppInstallation.fromMap(p))), + ); + } + + @override + Map toMap() { + return { + "total": total, + "installations": installations.map((p) => p.toMap()).toList(), + }; + } +} diff --git a/lib/src/models/file.dart b/lib/src/models/file.dart index 01cdcb3a..91c5e945 100644 --- a/lib/src/models/file.dart +++ b/lib/src/models/file.dart @@ -20,6 +20,12 @@ class File implements Model { /// File name. final String name; + /// Virtual folder containing the file, with a trailing slash. Empty for the bucket root. + final String folder; + + /// Full virtual path of the file: the folder followed by the file name. + final String key; + /// File MD5 signature. final String signature; @@ -51,6 +57,8 @@ class File implements Model { required this.$updatedAt, required this.$permissions, required this.name, + required this.folder, + required this.key, required this.signature, required this.mimeType, required this.sizeOriginal, @@ -69,6 +77,8 @@ class File implements Model { $updatedAt: map['\$updatedAt'].toString(), $permissions: List.from(map['\$permissions'] ?? []), name: map['name'].toString(), + folder: map['folder'].toString(), + key: map['key'].toString(), signature: map['signature'].toString(), mimeType: map['mimeType'].toString(), sizeOriginal: map['sizeOriginal'], @@ -89,6 +99,8 @@ class File implements Model { "\$updatedAt": $updatedAt, "\$permissions": $permissions, "name": name, + "folder": folder, + "key": key, "signature": signature, "mimeType": mimeType, "sizeOriginal": sizeOriginal, diff --git a/lib/src/models/oauth2_consent.dart b/lib/src/models/oauth2_consent.dart new file mode 100644 index 00000000..0f71fac2 --- /dev/null +++ b/lib/src/models/oauth2_consent.dart @@ -0,0 +1,78 @@ +part of '../../models.dart'; + +/// OAuth2 Consent +class Oauth2Consent implements Model { + /// Consent ID. + final String $id; + + /// Consent creation time in ISO 8601 format. + final String $createdAt; + + /// Consent update date in ISO 8601 format. + final String $updatedAt; + + /// ID of the user the consent belongs to. + final String userId; + + /// ID of the registered app the consent was given to. Empty for URL-form (CIMD) clients. + final String appId; + + /// Client ID metadata document URL of the client the consent was given to. Empty for registered apps. + final String cimdUrl; + + /// OAuth2 scopes the user consented to. + final List scopes; + + /// RFC 8707 resource indicators the user consented to. + final List resources; + + /// Authorization details the user consented to, as a JSON string. Each entry has a `type` plus project-defined fields. + final String authorizationDetails; + + /// Consent expiration time in ISO 8601 format. Empty when the consent has no token-bound expiry yet. + final String expire; + + Oauth2Consent({ + required this.$id, + required this.$createdAt, + required this.$updatedAt, + required this.userId, + required this.appId, + required this.cimdUrl, + required this.scopes, + required this.resources, + required this.authorizationDetails, + required this.expire, + }); + + factory Oauth2Consent.fromMap(Map map) { + return Oauth2Consent( + $id: map['\$id'].toString(), + $createdAt: map['\$createdAt'].toString(), + $updatedAt: map['\$updatedAt'].toString(), + userId: map['userId'].toString(), + appId: map['appId'].toString(), + cimdUrl: map['cimdUrl'].toString(), + scopes: List.from(map['scopes'] ?? []), + resources: List.from(map['resources'] ?? []), + authorizationDetails: map['authorizationDetails'].toString(), + expire: map['expire'].toString(), + ); + } + + @override + Map toMap() { + return { + "\$id": $id, + "\$createdAt": $createdAt, + "\$updatedAt": $updatedAt, + "userId": userId, + "appId": appId, + "cimdUrl": cimdUrl, + "scopes": scopes, + "resources": resources, + "authorizationDetails": authorizationDetails, + "expire": expire, + }; + } +} diff --git a/lib/src/models/oauth2_consent_list.dart b/lib/src/models/oauth2_consent_list.dart new file mode 100644 index 00000000..a5f355e1 --- /dev/null +++ b/lib/src/models/oauth2_consent_list.dart @@ -0,0 +1,31 @@ +part of '../../models.dart'; + +/// OAuth2 consents list +class Oauth2ConsentList implements Model { + /// Total number of consents that matched your query. + final int total; + + /// List of consents. + final List consents; + + Oauth2ConsentList({ + required this.total, + required this.consents, + }); + + factory Oauth2ConsentList.fromMap(Map map) { + return Oauth2ConsentList( + total: map['total'], + consents: List.from( + map['consents'].map((p) => Oauth2Consent.fromMap(p))), + ); + } + + @override + Map toMap() { + return { + "total": total, + "consents": consents.map((p) => p.toMap()).toList(), + }; + } +} diff --git a/lib/src/models/oauth2_consent_token.dart b/lib/src/models/oauth2_consent_token.dart new file mode 100644 index 00000000..adbee89f --- /dev/null +++ b/lib/src/models/oauth2_consent_token.dart @@ -0,0 +1,84 @@ +part of '../../models.dart'; + +/// OAuth2 Consent Token +class Oauth2ConsentToken implements Model { + /// Token family ID. + final String $id; + + /// Token creation time in ISO 8601 format. + final String $createdAt; + + /// Token update date in ISO 8601 format. Refreshing the token family updates this. + final String $updatedAt; + + /// ID of the consent the token family was issued under. + final String consentId; + + /// ID of the user the token family belongs to. + final String userId; + + /// ID of the registered app the token family was issued to. Empty for URL-form (CIMD) clients. + final String appId; + + /// Client ID metadata document URL of the client the token family was issued to. Empty for registered apps. + final String cimdUrl; + + /// OAuth2 scopes granted on the token family. + final List scopes; + + /// RFC 8707 resource indicators granted on the token family. + final List resources; + + /// Authorization details granted on the token family, as a JSON string. Each entry has a `type` plus project-defined fields. + final String authorizationDetails; + + /// Expiration time of the current access token of this family in ISO 8601 format. + final String expire; + + Oauth2ConsentToken({ + required this.$id, + required this.$createdAt, + required this.$updatedAt, + required this.consentId, + required this.userId, + required this.appId, + required this.cimdUrl, + required this.scopes, + required this.resources, + required this.authorizationDetails, + required this.expire, + }); + + factory Oauth2ConsentToken.fromMap(Map map) { + return Oauth2ConsentToken( + $id: map['\$id'].toString(), + $createdAt: map['\$createdAt'].toString(), + $updatedAt: map['\$updatedAt'].toString(), + consentId: map['consentId'].toString(), + userId: map['userId'].toString(), + appId: map['appId'].toString(), + cimdUrl: map['cimdUrl'].toString(), + scopes: List.from(map['scopes'] ?? []), + resources: List.from(map['resources'] ?? []), + authorizationDetails: map['authorizationDetails'].toString(), + expire: map['expire'].toString(), + ); + } + + @override + Map toMap() { + return { + "\$id": $id, + "\$createdAt": $createdAt, + "\$updatedAt": $updatedAt, + "consentId": consentId, + "userId": userId, + "appId": appId, + "cimdUrl": cimdUrl, + "scopes": scopes, + "resources": resources, + "authorizationDetails": authorizationDetails, + "expire": expire, + }; + } +} diff --git a/lib/src/models/oauth2_consent_token_list.dart b/lib/src/models/oauth2_consent_token_list.dart new file mode 100644 index 00000000..cc5cdc16 --- /dev/null +++ b/lib/src/models/oauth2_consent_token_list.dart @@ -0,0 +1,31 @@ +part of '../../models.dart'; + +/// OAuth2 consent tokens list +class Oauth2ConsentTokenList implements Model { + /// Total number of tokens that matched your query. + final int total; + + /// List of tokens. + final List tokens; + + Oauth2ConsentTokenList({ + required this.total, + required this.tokens, + }); + + factory Oauth2ConsentTokenList.fromMap(Map map) { + return Oauth2ConsentTokenList( + total: map['total'], + tokens: List.from( + map['tokens'].map((p) => Oauth2ConsentToken.fromMap(p))), + ); + } + + @override + Map toMap() { + return { + "total": total, + "tokens": tokens.map((p) => p.toMap()).toList(), + }; + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 7009bc3b..98866f5c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: appwrite -version: 25.3.0 +version: 25.4.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/services/account_test.dart b/test/services/account_test.dart index d5d61fe7..2752044f 100644 --- a/test/services/account_test.dart +++ b/test/services/account_test.dart @@ -117,6 +117,113 @@ void main() { expect(response, isA()); }); + test('test method listConsents()', () async { + final Map data = { + 'total': 5, + 'consents': [], + }; + + when(client.call( + HttpMethod.get, + )).thenAnswer((_) async => Response(data: data)); + + final response = await account.listConsents(); + expect(response, isA()); + }); + + test('test method getConsent()', () async { + final Map data = { + '\$id': '5e5ea5c16897e', + '\$createdAt': '2020-10-15T06:38:00.000+00:00', + '\$updatedAt': '2020-10-15T06:38:00.000+00:00', + 'userId': '5e5ea5c16897e', + 'appId': '5e5ea5c16897e', + 'cimdUrl': 'https://example.com/.well-known/client-metadata.json', + 'scopes': [], + 'resources': [], + 'authorizationDetails': + '[{\"type\":\"calendar\",\"identifier\":\"primary\",\"actions\":[\"read_events\",\"create_event\"]}]', + 'expire': '2020-10-15T06:38:00.000+00:00', + }; + + when(client.call( + HttpMethod.get, + )).thenAnswer((_) async => Response(data: data)); + + final response = await account.getConsent( + consentId: '', + ); + expect(response, isA()); + }); + + test('test method deleteConsent()', () async { + final data = ''; + + when(client.call( + HttpMethod.delete, + )).thenAnswer((_) async => Response(data: data)); + + final response = await account.deleteConsent( + consentId: '', + ); + }); + + test('test method listConsentTokens()', () async { + final Map data = { + 'total': 5, + 'tokens': [], + }; + + when(client.call( + HttpMethod.get, + )).thenAnswer((_) async => Response(data: data)); + + final response = await account.listConsentTokens( + consentId: '', + ); + expect(response, isA()); + }); + + test('test method getConsentToken()', () async { + final Map data = { + '\$id': '5e5ea5c16897e', + '\$createdAt': '2020-10-15T06:38:00.000+00:00', + '\$updatedAt': '2020-10-15T06:38:00.000+00:00', + 'consentId': '5e5ea5c16897e', + 'userId': '5e5ea5c16897e', + 'appId': '5e5ea5c16897e', + 'cimdUrl': 'https://example.com/.well-known/client-metadata.json', + 'scopes': [], + 'resources': [], + 'authorizationDetails': + '[{\"type\":\"calendar\",\"identifier\":\"primary\",\"actions\":[\"read_events\",\"create_event\"]}]', + 'expire': '2020-10-15T06:38:00.000+00:00', + }; + + when(client.call( + HttpMethod.get, + )).thenAnswer((_) async => Response(data: data)); + + final response = await account.getConsentToken( + consentId: '', + tokenId: '', + ); + expect(response, isA()); + }); + + test('test method deleteConsentToken()', () async { + final data = ''; + + when(client.call( + HttpMethod.delete, + )).thenAnswer((_) async => Response(data: data)); + + final response = await account.deleteConsentToken( + consentId: '', + tokenId: '', + ); + }); + test('test method updateEmail()', () async { final Map data = { '\$id': '5e5ea5c16897e', diff --git a/test/services/organization_test.dart b/test/services/organization_test.dart new file mode 100644 index 00000000..00455d84 --- /dev/null +++ b/test/services/organization_test.dart @@ -0,0 +1,155 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:appwrite/models.dart' as models; +import 'package:appwrite/enums.dart' as enums; +import 'package:appwrite/src/enums.dart'; +import 'package:appwrite/src/response.dart'; +import 'dart:typed_data'; +import 'package:appwrite/appwrite.dart'; + +class MockClient extends Mock implements Client { + Map config = {'project': 'testproject'}; + String endPoint = 'https://localhost/v1'; + @override + Future call( + HttpMethod? method, { + String path = '', + Map headers = const {}, + Map params = const {}, + ResponseType? responseType, + }) async { + return super.noSuchMethod(Invocation.method(#call, [method]), + returnValue: Response()); + } + + @override + Future webAuth( + Uri? url, { + String? callbackUrlScheme, + }) async { + return super + .noSuchMethod(Invocation.method(#webAuth, [url]), returnValue: 'done'); + } + + @override + Future chunkedUpload({ + String? path, + Map? params, + String? paramName, + String? idParamName, + Map? headers, + Function(UploadProgress)? onProgress, + }) async { + return super.noSuchMethod( + Invocation.method( + #chunkedUpload, [path, params, paramName, idParamName, headers]), + returnValue: Response(data: {})); + } +} + +void main() { + group('Organization test', () { + late MockClient client; + late Organization organization; + + setUp(() { + client = MockClient(); + organization = Organization(client); + }); + + test('test method listInstallations()', () async { + final Map data = { + 'total': 5, + 'installations': [], + }; + + when(client.call( + HttpMethod.get, + )).thenAnswer((_) async => Response(data: data)); + + final response = await organization.listInstallations(); + expect(response, isA()); + }); + + test('test method createInstallation()', () async { + final Map data = { + '\$id': '5e5ea5c16897e', + '\$createdAt': '2020-10-15T06:38:00.000+00:00', + '\$updatedAt': '2020-10-15T06:38:00.000+00:00', + 'appId': '5e5ea5c16897e', + 'teamId': '5e5ea5c16897e', + 'scopes': [], + 'authorizationDetails': {}, + 'createdById': '5e5ea5c16897e', + 'createdByName': 'Walter White', + }; + + when(client.call( + HttpMethod.post, + )).thenAnswer((_) async => Response(data: data)); + + final response = await organization.createInstallation( + appId: '', + ); + expect(response, isA()); + }); + + test('test method getInstallation()', () async { + final Map data = { + '\$id': '5e5ea5c16897e', + '\$createdAt': '2020-10-15T06:38:00.000+00:00', + '\$updatedAt': '2020-10-15T06:38:00.000+00:00', + 'appId': '5e5ea5c16897e', + 'teamId': '5e5ea5c16897e', + 'scopes': [], + 'authorizationDetails': {}, + 'createdById': '5e5ea5c16897e', + 'createdByName': 'Walter White', + }; + + when(client.call( + HttpMethod.get, + )).thenAnswer((_) async => Response(data: data)); + + final response = await organization.getInstallation( + installationId: '', + ); + expect(response, isA()); + }); + + test('test method updateInstallation()', () async { + final Map data = { + '\$id': '5e5ea5c16897e', + '\$createdAt': '2020-10-15T06:38:00.000+00:00', + '\$updatedAt': '2020-10-15T06:38:00.000+00:00', + 'appId': '5e5ea5c16897e', + 'teamId': '5e5ea5c16897e', + 'scopes': [], + 'authorizationDetails': {}, + 'createdById': '5e5ea5c16897e', + 'createdByName': 'Walter White', + }; + + when(client.call( + HttpMethod.put, + )).thenAnswer((_) async => Response(data: data)); + + final response = await organization.updateInstallation( + installationId: '', + ); + expect(response, isA()); + }); + + test('test method deleteInstallation()', () async { + final data = ''; + + when(client.call( + HttpMethod.delete, + )).thenAnswer((_) async => Response(data: data)); + + final response = await organization.deleteInstallation( + installationId: '', + ); + }); + }); +} diff --git a/test/services/storage_test.dart b/test/services/storage_test.dart index 55a42f94..9ee2b2c2 100644 --- a/test/services/storage_test.dart +++ b/test/services/storage_test.dart @@ -81,6 +81,8 @@ void main() { '\$updatedAt': '2020-10-15T06:38:00.000+00:00', '\$permissions': [], 'name': 'Pink.png', + 'folder': 'photos/2026/', + 'key': 'photos/2026/Pink.png', 'signature': '5d529fd02b544198ae075bd57c1762bb', 'mimeType': 'image/png', 'sizeOriginal': 17890, @@ -115,6 +117,8 @@ void main() { '\$updatedAt': '2020-10-15T06:38:00.000+00:00', '\$permissions': [], 'name': 'Pink.png', + 'folder': 'photos/2026/', + 'key': 'photos/2026/Pink.png', 'signature': '5d529fd02b544198ae075bd57c1762bb', 'mimeType': 'image/png', 'sizeOriginal': 17890, @@ -144,6 +148,8 @@ void main() { '\$updatedAt': '2020-10-15T06:38:00.000+00:00', '\$permissions': [], 'name': 'Pink.png', + 'folder': 'photos/2026/', + 'key': 'photos/2026/Pink.png', 'signature': '5d529fd02b544198ae075bd57c1762bb', 'mimeType': 'image/png', 'sizeOriginal': 17890, diff --git a/test/services/teams_test.dart b/test/services/teams_test.dart index 83f2667b..d308ebe3 100644 --- a/test/services/teams_test.dart +++ b/test/services/teams_test.dart @@ -145,6 +145,107 @@ void main() { ); }); + test('test method listInstallations()', () async { + final Map data = { + 'total': 5, + 'installations': [], + }; + + when(client.call( + HttpMethod.get, + )).thenAnswer((_) async => Response(data: data)); + + final response = await teams.listInstallations( + teamId: '', + ); + expect(response, isA()); + }); + + test('test method createInstallation()', () async { + final Map data = { + '\$id': '5e5ea5c16897e', + '\$createdAt': '2020-10-15T06:38:00.000+00:00', + '\$updatedAt': '2020-10-15T06:38:00.000+00:00', + 'appId': '5e5ea5c16897e', + 'teamId': '5e5ea5c16897e', + 'scopes': [], + 'authorizationDetails': {}, + 'createdById': '5e5ea5c16897e', + 'createdByName': 'Walter White', + }; + + when(client.call( + HttpMethod.post, + )).thenAnswer((_) async => Response(data: data)); + + final response = await teams.createInstallation( + teamId: '', + appId: '', + ); + expect(response, isA()); + }); + + test('test method getInstallation()', () async { + final Map data = { + '\$id': '5e5ea5c16897e', + '\$createdAt': '2020-10-15T06:38:00.000+00:00', + '\$updatedAt': '2020-10-15T06:38:00.000+00:00', + 'appId': '5e5ea5c16897e', + 'teamId': '5e5ea5c16897e', + 'scopes': [], + 'authorizationDetails': {}, + 'createdById': '5e5ea5c16897e', + 'createdByName': 'Walter White', + }; + + when(client.call( + HttpMethod.get, + )).thenAnswer((_) async => Response(data: data)); + + final response = await teams.getInstallation( + teamId: '', + installationId: '', + ); + expect(response, isA()); + }); + + test('test method updateInstallation()', () async { + final Map data = { + '\$id': '5e5ea5c16897e', + '\$createdAt': '2020-10-15T06:38:00.000+00:00', + '\$updatedAt': '2020-10-15T06:38:00.000+00:00', + 'appId': '5e5ea5c16897e', + 'teamId': '5e5ea5c16897e', + 'scopes': [], + 'authorizationDetails': {}, + 'createdById': '5e5ea5c16897e', + 'createdByName': 'Walter White', + }; + + when(client.call( + HttpMethod.put, + )).thenAnswer((_) async => Response(data: data)); + + final response = await teams.updateInstallation( + teamId: '', + installationId: '', + ); + expect(response, isA()); + }); + + test('test method deleteInstallation()', () async { + final data = ''; + + when(client.call( + HttpMethod.delete, + )).thenAnswer((_) async => Response(data: data)); + + final response = await teams.deleteInstallation( + teamId: '', + installationId: '', + ); + }); + test('test method listMemberships()', () async { final Map data = { 'total': 5, diff --git a/test/src/models/app_installation_list_test.dart b/test/src/models/app_installation_list_test.dart new file mode 100644 index 00000000..9a9955d4 --- /dev/null +++ b/test/src/models/app_installation_list_test.dart @@ -0,0 +1,19 @@ +import 'package:appwrite/models.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('AppInstallationList', () { + test('model', () { + final model = AppInstallationList( + total: 5, + installations: [], + ); + + final map = model.toMap(); + final result = AppInstallationList.fromMap(map); + + expect(result.total, 5); + expect(result.installations, []); + }); + }); +} diff --git a/test/src/models/app_installation_test.dart b/test/src/models/app_installation_test.dart new file mode 100644 index 00000000..dc0ec63c --- /dev/null +++ b/test/src/models/app_installation_test.dart @@ -0,0 +1,33 @@ +import 'package:appwrite/models.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('AppInstallation', () { + test('model', () { + final model = AppInstallation( + $id: '5e5ea5c16897e', + $createdAt: '2020-10-15T06:38:00.000+00:00', + $updatedAt: '2020-10-15T06:38:00.000+00:00', + appId: '5e5ea5c16897e', + teamId: '5e5ea5c16897e', + scopes: [], + authorizationDetails: {}, + createdById: '5e5ea5c16897e', + createdByName: 'Walter White', + ); + + final map = model.toMap(); + final result = AppInstallation.fromMap(map); + + expect(result.$id, '5e5ea5c16897e'); + expect(result.$createdAt, '2020-10-15T06:38:00.000+00:00'); + expect(result.$updatedAt, '2020-10-15T06:38:00.000+00:00'); + expect(result.appId, '5e5ea5c16897e'); + expect(result.teamId, '5e5ea5c16897e'); + expect(result.scopes, []); + expect(result.authorizationDetails, {}); + expect(result.createdById, '5e5ea5c16897e'); + expect(result.createdByName, 'Walter White'); + }); + }); +} diff --git a/test/src/models/file_test.dart b/test/src/models/file_test.dart index 7f132646..7bf20fce 100644 --- a/test/src/models/file_test.dart +++ b/test/src/models/file_test.dart @@ -11,6 +11,8 @@ void main() { $updatedAt: '2020-10-15T06:38:00.000+00:00', $permissions: [], name: 'Pink.png', + folder: 'photos/2026/', + key: 'photos/2026/Pink.png', signature: '5d529fd02b544198ae075bd57c1762bb', mimeType: 'image/png', sizeOriginal: 17890, @@ -30,6 +32,8 @@ void main() { expect(result.$updatedAt, '2020-10-15T06:38:00.000+00:00'); expect(result.$permissions, []); expect(result.name, 'Pink.png'); + expect(result.folder, 'photos/2026/'); + expect(result.key, 'photos/2026/Pink.png'); expect(result.signature, '5d529fd02b544198ae075bd57c1762bb'); expect(result.mimeType, 'image/png'); expect(result.sizeOriginal, 17890); diff --git a/test/src/models/oauth2_consent_list_test.dart b/test/src/models/oauth2_consent_list_test.dart new file mode 100644 index 00000000..be54297f --- /dev/null +++ b/test/src/models/oauth2_consent_list_test.dart @@ -0,0 +1,19 @@ +import 'package:appwrite/models.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('Oauth2ConsentList', () { + test('model', () { + final model = Oauth2ConsentList( + total: 5, + consents: [], + ); + + final map = model.toMap(); + final result = Oauth2ConsentList.fromMap(map); + + expect(result.total, 5); + expect(result.consents, []); + }); + }); +} diff --git a/test/src/models/oauth2_consent_test.dart b/test/src/models/oauth2_consent_test.dart new file mode 100644 index 00000000..32456d3c --- /dev/null +++ b/test/src/models/oauth2_consent_test.dart @@ -0,0 +1,38 @@ +import 'package:appwrite/models.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('Oauth2Consent', () { + test('model', () { + final model = Oauth2Consent( + $id: '5e5ea5c16897e', + $createdAt: '2020-10-15T06:38:00.000+00:00', + $updatedAt: '2020-10-15T06:38:00.000+00:00', + userId: '5e5ea5c16897e', + appId: '5e5ea5c16897e', + cimdUrl: 'https://example.com/.well-known/client-metadata.json', + scopes: [], + resources: [], + authorizationDetails: + '[{\"type\":\"calendar\",\"identifier\":\"primary\",\"actions\":[\"read_events\",\"create_event\"]}]', + expire: '2020-10-15T06:38:00.000+00:00', + ); + + final map = model.toMap(); + final result = Oauth2Consent.fromMap(map); + + expect(result.$id, '5e5ea5c16897e'); + expect(result.$createdAt, '2020-10-15T06:38:00.000+00:00'); + expect(result.$updatedAt, '2020-10-15T06:38:00.000+00:00'); + expect(result.userId, '5e5ea5c16897e'); + expect(result.appId, '5e5ea5c16897e'); + expect(result.cimdUrl, + 'https://example.com/.well-known/client-metadata.json'); + expect(result.scopes, []); + expect(result.resources, []); + expect(result.authorizationDetails, + '[{\"type\":\"calendar\",\"identifier\":\"primary\",\"actions\":[\"read_events\",\"create_event\"]}]'); + expect(result.expire, '2020-10-15T06:38:00.000+00:00'); + }); + }); +} diff --git a/test/src/models/oauth2_consent_token_list_test.dart b/test/src/models/oauth2_consent_token_list_test.dart new file mode 100644 index 00000000..1048af89 --- /dev/null +++ b/test/src/models/oauth2_consent_token_list_test.dart @@ -0,0 +1,19 @@ +import 'package:appwrite/models.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('Oauth2ConsentTokenList', () { + test('model', () { + final model = Oauth2ConsentTokenList( + total: 5, + tokens: [], + ); + + final map = model.toMap(); + final result = Oauth2ConsentTokenList.fromMap(map); + + expect(result.total, 5); + expect(result.tokens, []); + }); + }); +} diff --git a/test/src/models/oauth2_consent_token_test.dart b/test/src/models/oauth2_consent_token_test.dart new file mode 100644 index 00000000..e8a7b94b --- /dev/null +++ b/test/src/models/oauth2_consent_token_test.dart @@ -0,0 +1,40 @@ +import 'package:appwrite/models.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('Oauth2ConsentToken', () { + test('model', () { + final model = Oauth2ConsentToken( + $id: '5e5ea5c16897e', + $createdAt: '2020-10-15T06:38:00.000+00:00', + $updatedAt: '2020-10-15T06:38:00.000+00:00', + consentId: '5e5ea5c16897e', + userId: '5e5ea5c16897e', + appId: '5e5ea5c16897e', + cimdUrl: 'https://example.com/.well-known/client-metadata.json', + scopes: [], + resources: [], + authorizationDetails: + '[{\"type\":\"calendar\",\"identifier\":\"primary\",\"actions\":[\"read_events\",\"create_event\"]}]', + expire: '2020-10-15T06:38:00.000+00:00', + ); + + final map = model.toMap(); + final result = Oauth2ConsentToken.fromMap(map); + + expect(result.$id, '5e5ea5c16897e'); + expect(result.$createdAt, '2020-10-15T06:38:00.000+00:00'); + expect(result.$updatedAt, '2020-10-15T06:38:00.000+00:00'); + expect(result.consentId, '5e5ea5c16897e'); + expect(result.userId, '5e5ea5c16897e'); + expect(result.appId, '5e5ea5c16897e'); + expect(result.cimdUrl, + 'https://example.com/.well-known/client-metadata.json'); + expect(result.scopes, []); + expect(result.resources, []); + expect(result.authorizationDetails, + '[{\"type\":\"calendar\",\"identifier\":\"primary\",\"actions\":[\"read_events\",\"create_event\"]}]'); + expect(result.expire, '2020-10-15T06:38:00.000+00:00'); + }); + }); +}