diff --git a/lib/appwrite.dart b/lib/appwrite.dart index 68c1d899..35f90d27 100644 --- a/lib/appwrite.dart +++ b/lib/appwrite.dart @@ -6,6 +6,8 @@ import 'dart:math'; import 'dart:typed_data'; import 'models.dart' as models; +import 'src/call_handlers/offline_call_handler.dart'; +import 'src/call_params.dart'; import 'src/enums.dart'; import 'src/input_file.dart'; import 'src/service.dart'; diff --git a/lib/query.dart b/lib/query.dart index 6dcca4a4..ab361b1c 100644 --- a/lib/query.dart +++ b/lib/query.dart @@ -47,8 +47,8 @@ class Query { static String parseValues(dynamic value) => (value is String) ? '"$value"' : '$value'; - String method; - List<dynamic> params; + final String method; + final List<dynamic> params; factory Query.parse(String query) { if (!query.contains('(') || !query.contains(')')) { diff --git a/lib/services/account.dart b/lib/services/account.dart index c58831b8..d5597a2b 100644 --- a/lib/services/account.dart +++ b/lib/services/account.dart @@ -1,1165 +1,901 @@ part of appwrite; - /// The Account service allows you to authenticate and manage a user account. +/// The Account service allows you to authenticate and manage a user account. class Account extends Service { - Account(super.client); - - /// Get Account - /// - /// Get currently logged in user data as JSON object. - /// - Future<models.Account> get() async { - const String path = '/account'; - - final Map<String, dynamic> params = { - }; - - final Map<String, String> headers = { - 'content-type': 'application/json', - }; - - final cacheModel = '/account'; - final cacheKey = 'current'; - final cacheResponseIdKey = '\$id'; - final cacheResponseContainerKey = ''; - - final res = await client.call( - HttpMethod.get, - path: path, - params: params, - headers: headers, - cacheModel: cacheModel, - cacheKey: cacheKey, - cacheResponseIdKey: cacheResponseIdKey, - cacheResponseContainerKey: cacheResponseContainerKey, - ); - - return models.Account.fromMap(res.data); - - } - - /// Create Account - /// - /// Use this endpoint to allow a new user to register a new account in your - /// project. After the user registration completes successfully, you can use - /// the [/account/verfication](/docs/client/account#accountCreateVerification) - /// route to start verifying the user email address. To allow the new user to - /// login to their new account, you need to create a new [account - /// session](/docs/client/account#accountCreateSession). - /// - Future<models.Account> create({required String userId, required String email, required String password, String? name}) async { - const String path = '/account'; - - final Map<String, dynamic> params = { - 'userId': userId, - 'email': email, - 'password': password, - 'name': name, - }; - - final Map<String, String> headers = { - 'content-type': 'application/json', - }; - - final cacheModel = ''; - final cacheKey = ''; - final cacheResponseIdKey = '\$id'; - final cacheResponseContainerKey = ''; - - final res = await client.call( - HttpMethod.post, - path: path, - params: params, - headers: headers, - cacheModel: cacheModel, - cacheKey: cacheKey, - cacheResponseIdKey: cacheResponseIdKey, - cacheResponseContainerKey: cacheResponseContainerKey, - ); - - return models.Account.fromMap(res.data); - - } - - /// Update Email - /// - /// 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 - /// email endpoint again to send the confirmation email. For security measures, - /// user password is required to complete this request. - /// This endpoint can also be used to convert an anonymous account to a normal - /// one, by passing an email address and a new password. - /// - /// - Future<models.Account> updateEmail({required String email, required String password}) async { - const String path = '/account/email'; - - final Map<String, dynamic> params = { - 'email': email, - 'password': password, - }; - - final Map<String, String> headers = { - 'content-type': 'application/json', - }; - - final cacheModel = '/account'; - final cacheKey = 'current'; - final cacheResponseIdKey = '\$id'; - final cacheResponseContainerKey = ''; - - final res = await client.call( - HttpMethod.patch, - path: path, - params: params, - headers: headers, - cacheModel: cacheModel, - cacheKey: cacheKey, - cacheResponseIdKey: cacheResponseIdKey, - cacheResponseContainerKey: cacheResponseContainerKey, - ); - - return models.Account.fromMap(res.data); - - } - - /// Create JWT - /// - /// Use this endpoint to create a JSON Web Token. You can use the resulting JWT - /// to authenticate on behalf of the current user when working with the - /// Appwrite server-side API and SDKs. The JWT secret is valid for 15 minutes - /// from its creation and will be invalid if the user will logout in that time - /// frame. - /// - Future<models.Jwt> createJWT() async { - const String path = '/account/jwt'; - - final Map<String, dynamic> params = { - }; - - final Map<String, String> headers = { - 'content-type': 'application/json', - }; - - final cacheModel = ''; - final cacheKey = ''; - final cacheResponseIdKey = '\$id'; - final cacheResponseContainerKey = ''; - - final res = await client.call( - HttpMethod.post, - path: path, - params: params, - headers: headers, - cacheModel: cacheModel, - cacheKey: cacheKey, - cacheResponseIdKey: cacheResponseIdKey, - cacheResponseContainerKey: cacheResponseContainerKey, - ); - - return models.Jwt.fromMap(res.data); - - } - - /// List Logs - /// - /// Get currently logged in user list of latest security activity logs. Each - /// log returns user IP address, location and date and time of log. - /// - Future<models.LogList> listLogs({List<String>? queries}) async { - const String path = '/account/logs'; - - final Map<String, dynamic> params = { - 'queries': queries, - }; - - final Map<String, String> headers = { - 'content-type': 'application/json', - }; - - final cacheModel = ''; - final cacheKey = ''; - final cacheResponseIdKey = '\$id'; - final cacheResponseContainerKey = 'logs'; - - final res = await client.call( - HttpMethod.get, - path: path, - params: params, - headers: headers, - cacheModel: cacheModel, - cacheKey: cacheKey, - cacheResponseIdKey: cacheResponseIdKey, - cacheResponseContainerKey: cacheResponseContainerKey, - ); - - return models.LogList.fromMap(res.data); - - } - - /// Update Name - /// - /// Update currently logged in user account name. - /// - Future<models.Account> updateName({required String name}) async { - const String path = '/account/name'; - - final Map<String, dynamic> params = { - 'name': name, - }; - - final Map<String, String> headers = { - 'content-type': 'application/json', - }; - - final cacheModel = '/account'; - final cacheKey = 'current'; - final cacheResponseIdKey = '\$id'; - final cacheResponseContainerKey = ''; - - final res = await client.call( - HttpMethod.patch, - path: path, - params: params, - headers: headers, - cacheModel: cacheModel, - cacheKey: cacheKey, - cacheResponseIdKey: cacheResponseIdKey, - cacheResponseContainerKey: cacheResponseContainerKey, - ); - - return models.Account.fromMap(res.data); - - } - - /// Update Password - /// - /// Update currently logged in user password. For validation, user is required - /// to pass in the new password, and the old password. For users created with - /// OAuth, Team Invites and Magic URL, oldPassword is optional. - /// - Future<models.Account> updatePassword({required String password, String? oldPassword}) async { - const String path = '/account/password'; - - final Map<String, dynamic> params = { - 'password': password, - 'oldPassword': oldPassword, - }; - - final Map<String, String> headers = { - 'content-type': 'application/json', - }; - - final cacheModel = '/account'; - final cacheKey = 'current'; - final cacheResponseIdKey = '\$id'; - final cacheResponseContainerKey = ''; - - final res = await client.call( - HttpMethod.patch, - path: path, - params: params, - headers: headers, - cacheModel: cacheModel, - cacheKey: cacheKey, - cacheResponseIdKey: cacheResponseIdKey, - cacheResponseContainerKey: cacheResponseContainerKey, - ); - - return models.Account.fromMap(res.data); - - } - - /// Update Phone - /// - /// Update the currently logged in user's phone number. After updating the - /// phone number, the phone verification status will be reset. A confirmation - /// SMS is not sent automatically, however you can use the [POST - /// /account/verification/phone](/docs/client/account#accountCreatePhoneVerification) - /// endpoint to send a confirmation SMS. - /// - Future<models.Account> updatePhone({required String phone, required String password}) async { - const String path = '/account/phone'; - - final Map<String, dynamic> params = { - 'phone': phone, - 'password': password, - }; - - final Map<String, String> headers = { - 'content-type': 'application/json', - }; - - final cacheModel = '/account'; - final cacheKey = 'current'; - final cacheResponseIdKey = '\$id'; - final cacheResponseContainerKey = ''; - - final res = await client.call( - HttpMethod.patch, - path: path, - params: params, - headers: headers, - cacheModel: cacheModel, - cacheKey: cacheKey, - cacheResponseIdKey: cacheResponseIdKey, - cacheResponseContainerKey: cacheResponseContainerKey, - ); - - return models.Account.fromMap(res.data); - - } - - /// Get Account Preferences - /// - /// Get currently logged in user preferences as a key-value object. - /// - Future<models.Preferences> getPrefs() async { - const String path = '/account/prefs'; - - final Map<String, dynamic> params = { - }; - - final Map<String, String> headers = { - 'content-type': 'application/json', - }; - - final cacheModel = '/account/prefs'; - final cacheKey = 'current'; - final cacheResponseIdKey = '\$id'; - final cacheResponseContainerKey = ''; - - final res = await client.call( - HttpMethod.get, - path: path, - params: params, - headers: headers, - cacheModel: cacheModel, - cacheKey: cacheKey, - cacheResponseIdKey: cacheResponseIdKey, - cacheResponseContainerKey: cacheResponseContainerKey, - ); - - return models.Preferences.fromMap(res.data); - - } - - /// Update Preferences - /// - /// Update currently logged in user account preferences. The object you pass is - /// stored as is, and replaces any previous value. The maximum allowed prefs - /// size is 64kB and throws error if exceeded. - /// - Future<models.Account> updatePrefs({required Map prefs}) async { - const String path = '/account/prefs'; - - final Map<String, dynamic> params = { - 'prefs': prefs, - }; - - final Map<String, String> headers = { - 'content-type': 'application/json', - }; - - final cacheModel = '/account/prefs'; - final cacheKey = 'current'; - final cacheResponseIdKey = '\$id'; - final cacheResponseContainerKey = ''; - - final res = await client.call( - HttpMethod.patch, - path: path, - params: params, - headers: headers, - cacheModel: cacheModel, - cacheKey: cacheKey, - cacheResponseIdKey: cacheResponseIdKey, - cacheResponseContainerKey: cacheResponseContainerKey, - ); - - return models.Account.fromMap(res.data); - - } - - /// Create Password Recovery - /// - /// Sends the user an email with a temporary secret key for password reset. - /// When the user clicks the confirmation link he is redirected back to your - /// app password reset URL with the secret key and email address values - /// attached to the URL query string. Use the query string params to submit a - /// request to the [PUT - /// /account/recovery](/docs/client/account#accountUpdateRecovery) endpoint to - /// complete the process. The verification link sent to the user's email - /// address is valid for 1 hour. - /// - Future<models.Token> createRecovery({required String email, required String url}) async { - const String path = '/account/recovery'; - - final Map<String, dynamic> params = { - 'email': email, - 'url': url, - }; - - final Map<String, String> headers = { - 'content-type': 'application/json', - }; - - final cacheModel = ''; - final cacheKey = ''; - final cacheResponseIdKey = '\$id'; - final cacheResponseContainerKey = ''; - - final res = await client.call( - HttpMethod.post, - path: path, - params: params, - headers: headers, - cacheModel: cacheModel, - cacheKey: cacheKey, - cacheResponseIdKey: cacheResponseIdKey, - cacheResponseContainerKey: cacheResponseContainerKey, - ); - - return models.Token.fromMap(res.data); - - } - - /// Create Password Recovery (confirmation) - /// - /// Use this endpoint to complete the user account password reset. Both the - /// **userId** and **secret** arguments will be passed as query parameters to - /// the redirect URL you have provided when sending your request to the [POST - /// /account/recovery](/docs/client/account#accountCreateRecovery) endpoint. - /// - /// Please note that in order to avoid a [Redirect - /// Attack](https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) - /// the only valid redirect URLs are the ones from domains you have set when - /// adding your platforms in the console interface. - /// - Future<models.Token> updateRecovery({required String userId, required String secret, required String password, required String passwordAgain}) async { - const String path = '/account/recovery'; - - final Map<String, dynamic> params = { - 'userId': userId, - 'secret': secret, - 'password': password, - 'passwordAgain': passwordAgain, - }; - - final Map<String, String> headers = { - 'content-type': 'application/json', - }; - - final cacheModel = ''; - final cacheKey = ''; - final cacheResponseIdKey = '\$id'; - final cacheResponseContainerKey = ''; - - final res = await client.call( - HttpMethod.put, - path: path, - params: params, - headers: headers, - cacheModel: cacheModel, - cacheKey: cacheKey, - cacheResponseIdKey: cacheResponseIdKey, - cacheResponseContainerKey: cacheResponseContainerKey, - ); - - return models.Token.fromMap(res.data); - - } - - /// List Sessions - /// - /// Get currently logged in user list of active sessions across different - /// devices. - /// - Future<models.SessionList> listSessions() async { - const String path = '/account/sessions'; - - final Map<String, dynamic> params = { - }; - - final Map<String, String> headers = { - 'content-type': 'application/json', - }; - - final cacheModel = '/account/sessions'; - final cacheKey = ''; - final cacheResponseIdKey = '\$id'; - final cacheResponseContainerKey = 'sessions'; - - final res = await client.call( - HttpMethod.get, - path: path, - params: params, - headers: headers, - cacheModel: cacheModel, - cacheKey: cacheKey, - cacheResponseIdKey: cacheResponseIdKey, - cacheResponseContainerKey: cacheResponseContainerKey, - ); - - return models.SessionList.fromMap(res.data); - - } - - /// Delete Sessions - /// - /// Delete all sessions from the user account and remove any sessions cookies - /// from the end client. - /// - Future deleteSessions() async { - const String path = '/account/sessions'; - - final Map<String, dynamic> params = { - }; - - final Map<String, String> headers = { - 'content-type': 'application/json', - }; - - final cacheModel = ''; - final cacheKey = ''; - final cacheResponseIdKey = '\$id'; - final cacheResponseContainerKey = ''; - - final res = await client.call( - HttpMethod.delete, - path: path, - params: params, - headers: headers, - cacheModel: cacheModel, - cacheKey: cacheKey, - cacheResponseIdKey: cacheResponseIdKey, - cacheResponseContainerKey: cacheResponseContainerKey, - ); - - return res.data; - - } - - /// Create Anonymous Session - /// - /// Use this endpoint to allow a new user to register an anonymous account in - /// your project. This route will also create a new session for the user. To - /// allow the new user to convert an anonymous account to a normal account, you - /// need to update its [email and - /// password](/docs/client/account#accountUpdateEmail) or create an [OAuth2 - /// session](/docs/client/account#accountCreateOAuth2Session). - /// - Future<models.Session> createAnonymousSession() async { - const String path = '/account/sessions/anonymous'; - - final Map<String, dynamic> params = { - }; - - final Map<String, String> headers = { - 'content-type': 'application/json', - }; - - final cacheModel = ''; - final cacheKey = ''; - final cacheResponseIdKey = '\$id'; - final cacheResponseContainerKey = ''; - - final res = await client.call( - HttpMethod.post, - path: path, - params: params, - headers: headers, - cacheModel: cacheModel, - cacheKey: cacheKey, - cacheResponseIdKey: cacheResponseIdKey, - cacheResponseContainerKey: cacheResponseContainerKey, - ); - - return models.Session.fromMap(res.data); - - } - - /// Create Email Session - /// - /// Allow the user to login into their account by providing a valid email and - /// password combination. This route will create a new session for the user. - /// - /// A user is limited to 10 active sessions at a time by default. [Learn more - /// about session limits](/docs/authentication#limits). - /// - Future<models.Session> createEmailSession({required String email, required String password}) async { - const String path = '/account/sessions/email'; - - final Map<String, dynamic> params = { - 'email': email, - 'password': password, - }; - - final Map<String, String> headers = { - 'content-type': 'application/json', - }; - - final cacheModel = ''; - final cacheKey = ''; - final cacheResponseIdKey = '\$id'; - final cacheResponseContainerKey = ''; - - final res = await client.call( - HttpMethod.post, - path: path, - params: params, - headers: headers, - cacheModel: cacheModel, - cacheKey: cacheKey, - cacheResponseIdKey: cacheResponseIdKey, - cacheResponseContainerKey: cacheResponseContainerKey, - ); - - return models.Session.fromMap(res.data); - - } - - /// Create Magic URL session - /// - /// Sends the user an email with a secret key for creating a session. If the - /// provided user ID has not be registered, a new user will be created. When - /// the user clicks the link in the email, the user is redirected back to the - /// URL you provided with the secret key and userId values attached to the URL - /// query string. Use the query string parameters to submit a request to the - /// [PUT - /// /account/sessions/magic-url](/docs/client/account#accountUpdateMagicURLSession) - /// endpoint to complete the login process. The link sent to the user's email - /// address is valid for 1 hour. If you are on a mobile device you can leave - /// the URL parameter empty, so that the login completion will be handled by - /// your Appwrite instance by default. - /// - /// A user is limited to 10 active sessions at a time by default. [Learn more - /// about session limits](/docs/authentication#limits). - /// - Future<models.Token> createMagicURLSession({required String userId, required String email, String? url}) async { - const String path = '/account/sessions/magic-url'; - - final Map<String, dynamic> params = { - 'userId': userId, - 'email': email, - 'url': url, - }; - - final Map<String, String> headers = { - 'content-type': 'application/json', - }; - - final cacheModel = ''; - final cacheKey = ''; - final cacheResponseIdKey = '\$id'; - final cacheResponseContainerKey = ''; - - final res = await client.call( - HttpMethod.post, - path: path, - params: params, - headers: headers, - cacheModel: cacheModel, - cacheKey: cacheKey, - cacheResponseIdKey: cacheResponseIdKey, - cacheResponseContainerKey: cacheResponseContainerKey, - ); - - return models.Token.fromMap(res.data); - - } - - /// Create Magic URL session (confirmation) - /// - /// Use this endpoint to complete creating the session with the Magic URL. Both - /// the **userId** and **secret** arguments will be passed as query parameters - /// to the redirect URL you have provided when sending your request to the - /// [POST - /// /account/sessions/magic-url](/docs/client/account#accountCreateMagicURLSession) - /// endpoint. - /// - /// Please note that in order to avoid a [Redirect - /// Attack](https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) - /// the only valid redirect URLs are the ones from domains you have set when - /// adding your platforms in the console interface. - /// - Future<models.Session> updateMagicURLSession({required String userId, required String secret}) async { - const String path = '/account/sessions/magic-url'; - - final Map<String, dynamic> params = { - 'userId': userId, - 'secret': secret, - }; - - final Map<String, String> headers = { - 'content-type': 'application/json', - }; - - final cacheModel = ''; - final cacheKey = ''; - final cacheResponseIdKey = '\$id'; - final cacheResponseContainerKey = ''; - - final res = await client.call( - HttpMethod.put, - path: path, - params: params, - headers: headers, - cacheModel: cacheModel, - cacheKey: cacheKey, - cacheResponseIdKey: cacheResponseIdKey, - cacheResponseContainerKey: cacheResponseContainerKey, - ); - - return models.Session.fromMap(res.data); - - } - - /// Create OAuth2 Session - /// - /// Allow the user to login to their account using the OAuth2 provider of their - /// choice. Each OAuth2 provider should be enabled from the Appwrite console - /// first. Use the success and failure arguments to provide a redirect URL's - /// back to your app when login is completed. - /// - /// If there is already an active session, the new session will be attached to - /// the logged-in account. If there are no active sessions, the server will - /// attempt to look for a user with the same email address as the email - /// received from the OAuth2 provider and attach the new session to the - /// existing user. If no matching user is found - the server will create a new - /// user. - /// - /// A user is limited to 10 active sessions at a time by default. [Learn more - /// about session limits](/docs/authentication#limits). - /// - /// - Future createOAuth2Session({required String provider, String? success, String? failure, List<String>? scopes}) async { - final String path = '/account/sessions/oauth2/{provider}'.replaceAll('{provider}', provider); - - final Map<String, dynamic> params = { - - 'success': success, - 'failure': failure, - 'scopes': scopes, - - 'project': client.config['project'], - }; - - final List query = []; - - params.forEach((key, value) { - if (value is List) { - for (var item in value) { - query.add(Uri.encodeComponent(key + '[]') + '=' + Uri.encodeComponent(item)); - } - } else if(value != null) { - query.add(Uri.encodeComponent(key) + '=' + Uri.encodeComponent(value)); - } - }); - - Uri endpoint = Uri.parse(client.endPoint); - Uri url = Uri(scheme: endpoint.scheme, - host: endpoint.host, - port: endpoint.port, - path: endpoint.path + path, - query: query.join('&') - ); - - return client.webAuth(url, callbackUrlScheme: success); - } - - /// Create Phone session - /// - /// Sends the user an SMS with a secret key for creating a session. If the - /// provided user ID has not be registered, a new user will be created. Use the - /// returned user ID and secret and submit a request to the [PUT - /// /account/sessions/phone](/docs/client/account#accountUpdatePhoneSession) - /// endpoint to complete the login process. The secret sent to the user's phone - /// is valid for 15 minutes. - /// - /// A user is limited to 10 active sessions at a time by default. [Learn more - /// about session limits](/docs/authentication#limits). - /// - Future<models.Token> createPhoneSession({required String userId, required String phone}) async { - const String path = '/account/sessions/phone'; - - final Map<String, dynamic> params = { - 'userId': userId, - 'phone': phone, - }; - - final Map<String, String> headers = { - 'content-type': 'application/json', - }; - - final cacheModel = ''; - final cacheKey = ''; - final cacheResponseIdKey = '\$id'; - final cacheResponseContainerKey = ''; - - final res = await client.call( - HttpMethod.post, - path: path, - params: params, - headers: headers, - cacheModel: cacheModel, - cacheKey: cacheKey, - cacheResponseIdKey: cacheResponseIdKey, - cacheResponseContainerKey: cacheResponseContainerKey, - ); - - return models.Token.fromMap(res.data); - - } - - /// Create Phone Session (confirmation) - /// - /// Use this endpoint to complete creating a session with SMS. Use the - /// **userId** from the - /// [createPhoneSession](/docs/client/account#accountCreatePhoneSession) - /// endpoint and the **secret** received via SMS to successfully update and - /// confirm the phone session. - /// - Future<models.Session> updatePhoneSession({required String userId, required String secret}) async { - const String path = '/account/sessions/phone'; - - final Map<String, dynamic> params = { - 'userId': userId, - 'secret': secret, - }; - - final Map<String, String> headers = { - 'content-type': 'application/json', - }; - - final cacheModel = ''; - final cacheKey = ''; - final cacheResponseIdKey = '\$id'; - final cacheResponseContainerKey = ''; - - final res = await client.call( - HttpMethod.put, - path: path, - params: params, - headers: headers, - cacheModel: cacheModel, - cacheKey: cacheKey, - cacheResponseIdKey: cacheResponseIdKey, - cacheResponseContainerKey: cacheResponseContainerKey, - ); - - return models.Session.fromMap(res.data); - - } - - /// Get Session - /// - /// Use this endpoint to get a logged in user's session using a Session ID. - /// Inputting 'current' will return the current session being used. - /// - Future<models.Session> getSession({required String sessionId}) async { - final String path = '/account/sessions/{sessionId}'.replaceAll('{sessionId}', sessionId); - - final Map<String, dynamic> params = { - }; - - final Map<String, String> headers = { - 'content-type': 'application/json', - }; - - final cacheModel = '/account/sessions'.replaceAll('{sessionId}', sessionId); - final cacheKey = sessionId; - final cacheResponseIdKey = '\$id'; - final cacheResponseContainerKey = ''; - - final res = await client.call( - HttpMethod.get, - path: path, - params: params, - headers: headers, - cacheModel: cacheModel, - cacheKey: cacheKey, - cacheResponseIdKey: cacheResponseIdKey, - cacheResponseContainerKey: cacheResponseContainerKey, - ); - - return models.Session.fromMap(res.data); - - } - - /// Update OAuth Session (Refresh Tokens) - /// - /// Access tokens have limited lifespan and expire to mitigate security risks. - /// If session was created using an OAuth provider, this route can be used to - /// "refresh" the access token. - /// - Future<models.Session> updateSession({required String sessionId}) async { - final String path = '/account/sessions/{sessionId}'.replaceAll('{sessionId}', sessionId); - - final Map<String, dynamic> params = { - }; - - final Map<String, String> headers = { - 'content-type': 'application/json', - }; - - final cacheModel = ''; - final cacheKey = ''; - final cacheResponseIdKey = '\$id'; - final cacheResponseContainerKey = ''; - - final res = await client.call( - HttpMethod.patch, - path: path, - params: params, - headers: headers, - cacheModel: cacheModel, - cacheKey: cacheKey, - cacheResponseIdKey: cacheResponseIdKey, - cacheResponseContainerKey: cacheResponseContainerKey, - ); - - return models.Session.fromMap(res.data); - - } - - /// Delete Session - /// - /// Use this endpoint to log out the currently logged in user from all their - /// account sessions across all of their different devices. When using the - /// Session ID argument, only the unique session ID provided is deleted. - /// - /// - Future deleteSession({required String sessionId}) async { - final String path = '/account/sessions/{sessionId}'.replaceAll('{sessionId}', sessionId); - - final Map<String, dynamic> params = { - }; - - final Map<String, String> headers = { - 'content-type': 'application/json', - }; - - final cacheModel = ''; - final cacheKey = ''; - final cacheResponseIdKey = '\$id'; - final cacheResponseContainerKey = ''; - - final res = await client.call( - HttpMethod.delete, - path: path, - params: params, - headers: headers, - cacheModel: cacheModel, - cacheKey: cacheKey, - cacheResponseIdKey: cacheResponseIdKey, - cacheResponseContainerKey: cacheResponseContainerKey, - ); - - return res.data; - - } - - /// Update Status - /// - /// Block the currently logged in user account. Behind the scene, the user - /// record is not deleted but permanently blocked from any access. To - /// completely delete a user, use the Users API instead. - /// - Future<models.Account> updateStatus() async { - const String path = '/account/status'; - - final Map<String, dynamic> params = { - }; - - final Map<String, String> headers = { - 'content-type': 'application/json', - }; - - final cacheModel = ''; - final cacheKey = ''; - final cacheResponseIdKey = '\$id'; - final cacheResponseContainerKey = ''; - - final res = await client.call( - HttpMethod.patch, - path: path, - params: params, - headers: headers, - cacheModel: cacheModel, - cacheKey: cacheKey, - cacheResponseIdKey: cacheResponseIdKey, - cacheResponseContainerKey: cacheResponseContainerKey, - ); - - return models.Account.fromMap(res.data); - - } - - /// Create Email Verification - /// - /// Use this endpoint to send a verification message to your user email address - /// to confirm they are the valid owners of that address. Both the **userId** - /// and **secret** arguments will be passed as query parameters to the URL you - /// have provided to be attached to the verification email. The provided URL - /// should redirect the user back to your app and allow you to complete the - /// verification process by verifying both the **userId** and **secret** - /// parameters. Learn more about how to [complete the verification - /// process](/docs/client/account#accountUpdateEmailVerification). The - /// verification link sent to the user's email address is valid for 7 days. - /// - /// Please note that in order to avoid a [Redirect - /// Attack](https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md), - /// the only valid redirect URLs are the ones from domains you have set when - /// adding your platforms in the console interface. - /// - /// - Future<models.Token> createVerification({required String url}) async { - const String path = '/account/verification'; - - final Map<String, dynamic> params = { - 'url': url, - }; - - final Map<String, String> headers = { - 'content-type': 'application/json', - }; - - final cacheModel = ''; - final cacheKey = ''; - final cacheResponseIdKey = '\$id'; - final cacheResponseContainerKey = ''; - - final res = await client.call( - HttpMethod.post, - path: path, - params: params, - headers: headers, - cacheModel: cacheModel, - cacheKey: cacheKey, - cacheResponseIdKey: cacheResponseIdKey, - cacheResponseContainerKey: cacheResponseContainerKey, - ); - - return models.Token.fromMap(res.data); - - } - - /// Create Email Verification (confirmation) - /// - /// Use this endpoint to complete the user email verification process. Use both - /// the **userId** and **secret** parameters that were attached to your app URL - /// to verify the user email ownership. If confirmed this route will return a - /// 200 status code. - /// - Future<models.Token> updateVerification({required String userId, required String secret}) async { - const String path = '/account/verification'; - - final Map<String, dynamic> params = { - 'userId': userId, - 'secret': secret, - }; - - final Map<String, String> headers = { - 'content-type': 'application/json', - }; - - final cacheModel = ''; - final cacheKey = ''; - final cacheResponseIdKey = '\$id'; - final cacheResponseContainerKey = ''; - - final res = await client.call( - HttpMethod.put, - path: path, - params: params, - headers: headers, - cacheModel: cacheModel, - cacheKey: cacheKey, - cacheResponseIdKey: cacheResponseIdKey, - cacheResponseContainerKey: cacheResponseContainerKey, - ); - - return models.Token.fromMap(res.data); - - } - - /// Create Phone Verification - /// - /// Use this endpoint to send a verification SMS to the currently logged in - /// user. This endpoint is meant for use after updating a user's phone number - /// using the [accountUpdatePhone](/docs/client/account#accountUpdatePhone) - /// endpoint. Learn more about how to [complete the verification - /// process](/docs/client/account#accountUpdatePhoneVerification). The - /// verification code sent to the user's phone number is valid for 15 minutes. - /// - Future<models.Token> createPhoneVerification() async { - const String path = '/account/verification/phone'; - - final Map<String, dynamic> params = { - }; - - final Map<String, String> headers = { - 'content-type': 'application/json', - }; - - final cacheModel = ''; - final cacheKey = ''; - final cacheResponseIdKey = '\$id'; - final cacheResponseContainerKey = ''; - - final res = await client.call( - HttpMethod.post, - path: path, - params: params, - headers: headers, - cacheModel: cacheModel, - cacheKey: cacheKey, - cacheResponseIdKey: cacheResponseIdKey, - cacheResponseContainerKey: cacheResponseContainerKey, - ); - - return models.Token.fromMap(res.data); - - } - - /// Create Phone Verification (confirmation) - /// - /// Use this endpoint to complete the user phone verification process. Use the - /// **userId** and **secret** that were sent to your user's phone number to - /// verify the user email ownership. If confirmed this route will return a 200 - /// status code. - /// - Future<models.Token> updatePhoneVerification({required String userId, required String secret}) async { - const String path = '/account/verification/phone'; - - final Map<String, dynamic> params = { - 'userId': userId, - 'secret': secret, - }; - - final Map<String, String> headers = { - 'content-type': 'application/json', - }; - - final cacheModel = ''; - final cacheKey = ''; - final cacheResponseIdKey = '\$id'; - final cacheResponseContainerKey = ''; - - final res = await client.call( - HttpMethod.put, - path: path, - params: params, - headers: headers, - cacheModel: cacheModel, - cacheKey: cacheKey, - cacheResponseIdKey: cacheResponseIdKey, - cacheResponseContainerKey: cacheResponseContainerKey, - ); - - return models.Token.fromMap(res.data); - - } -} \ No newline at end of file + Account(super.client); + + /// Get Account + /// + /// Get currently logged in user data as JSON object. + /// + Future<models.Account> get() async { + const String path = '/account'; + + final Map<String, dynamic> params = {}; + + final Map<String, String> headers = { + 'content-type': 'application/json', + }; + + final res = await client.call(CallParams( + HttpMethod.get, + path, + params: params, + headers: headers, + )); + + return models.Account.fromMap(res.data); + } + + /// Create Account + /// + /// Use this endpoint to allow a new user to register a new account in your + /// project. After the user registration completes successfully, you can use + /// the [/account/verfication](/docs/client/account#accountCreateVerification) + /// route to start verifying the user email address. To allow the new user to + /// login to their new account, you need to create a new [account + /// session](/docs/client/account#accountCreateSession). + /// + Future<models.Account> create( + {required String userId, + required String email, + required String password, + String? name}) async { + const String path = '/account'; + + final Map<String, dynamic> params = { + 'userId': userId, + 'email': email, + 'password': password, + 'name': name, + }; + + final Map<String, String> headers = { + 'content-type': 'application/json', + }; + + final res = await client.call(CallParams( + HttpMethod.post, + path, + params: params, + headers: headers, + )); + + return models.Account.fromMap(res.data); + } + + /// Update Email + /// + /// 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 + /// email endpoint again to send the confirmation email. For security measures, + /// user password is required to complete this request. + /// This endpoint can also be used to convert an anonymous account to a normal + /// one, by passing an email address and a new password. + /// + /// + Future<models.Account> updateEmail( + {required String email, required String password}) async { + const String path = '/account/email'; + + final Map<String, dynamic> params = { + 'email': email, + 'password': password, + }; + + final Map<String, String> headers = { + 'content-type': 'application/json', + }; + + final res = await client.call(CallParams( + HttpMethod.patch, + path, + params: params, + headers: headers, + )); + + return models.Account.fromMap(res.data); + } + + /// Create JWT + /// + /// Use this endpoint to create a JSON Web Token. You can use the resulting JWT + /// to authenticate on behalf of the current user when working with the + /// Appwrite server-side API and SDKs. The JWT secret is valid for 15 minutes + /// from its creation and will be invalid if the user will logout in that time + /// frame. + /// + Future<models.Jwt> createJWT() async { + const String path = '/account/jwt'; + + final Map<String, dynamic> params = {}; + + final Map<String, String> headers = { + 'content-type': 'application/json', + }; + + final res = await client.call(CallParams( + HttpMethod.post, + path, + params: params, + headers: headers, + )); + + return models.Jwt.fromMap(res.data); + } + + /// List Logs + /// + /// Get currently logged in user list of latest security activity logs. Each + /// log returns user IP address, location and date and time of log. + /// + Future<models.LogList> listLogs({List<String>? queries}) async { + const String path = '/account/logs'; + + final Map<String, dynamic> params = { + 'queries': queries, + }; + + final Map<String, String> headers = { + 'content-type': 'application/json', + }; + + final res = await client.call(CallParams( + HttpMethod.get, + path, + params: params, + headers: headers, + )); + + return models.LogList.fromMap(res.data); + } + + /// Update Name + /// + /// Update currently logged in user account name. + /// + Future<models.Account> updateName({required String name}) async { + const String path = '/account/name'; + + final Map<String, dynamic> params = { + 'name': name, + }; + + final Map<String, String> headers = { + 'content-type': 'application/json', + }; + + final res = await client.call(CallParams( + HttpMethod.patch, + path, + params: params, + headers: headers, + )); + + return models.Account.fromMap(res.data); + } + + /// Update Password + /// + /// Update currently logged in user password. For validation, user is required + /// to pass in the new password, and the old password. For users created with + /// OAuth, Team Invites and Magic URL, oldPassword is optional. + /// + Future<models.Account> updatePassword( + {required String password, String? oldPassword}) async { + const String path = '/account/password'; + + final Map<String, dynamic> params = { + 'password': password, + 'oldPassword': oldPassword, + }; + + final Map<String, String> headers = { + 'content-type': 'application/json', + }; + + final res = await client.call(CallParams( + HttpMethod.patch, + path, + params: params, + headers: headers, + )); + + return models.Account.fromMap(res.data); + } + + /// Update Phone + /// + /// Update the currently logged in user's phone number. After updating the + /// phone number, the phone verification status will be reset. A confirmation + /// SMS is not sent automatically, however you can use the [POST + /// /account/verification/phone](/docs/client/account#accountCreatePhoneVerification) + /// endpoint to send a confirmation SMS. + /// + Future<models.Account> updatePhone( + {required String phone, required String password}) async { + const String path = '/account/phone'; + + final Map<String, dynamic> params = { + 'phone': phone, + 'password': password, + }; + + final Map<String, String> headers = { + 'content-type': 'application/json', + }; + + final res = await client.call(CallParams( + HttpMethod.patch, + path, + params: params, + headers: headers, + )); + + return models.Account.fromMap(res.data); + } + + /// Get Account Preferences + /// + /// Get currently logged in user preferences as a key-value object. + /// + Future<models.Preferences> getPrefs() async { + const String path = '/account/prefs'; + + final Map<String, dynamic> params = {}; + + final Map<String, String> headers = { + 'content-type': 'application/json', + }; + + final res = await client.call(CallParams( + HttpMethod.get, + path, + params: params, + headers: headers, + )); + + return models.Preferences.fromMap(res.data); + } + + /// Update Preferences + /// + /// Update currently logged in user account preferences. The object you pass is + /// stored as is, and replaces any previous value. The maximum allowed prefs + /// size is 64kB and throws error if exceeded. + /// + Future<models.Account> updatePrefs({required Map prefs}) async { + const String path = '/account/prefs'; + + final Map<String, dynamic> params = { + 'prefs': prefs, + }; + + final Map<String, String> headers = { + 'content-type': 'application/json', + }; + + final res = await client.call(CallParams( + HttpMethod.patch, + path, + params: params, + headers: headers, + )); + + return models.Account.fromMap(res.data); + } + + /// Create Password Recovery + /// + /// Sends the user an email with a temporary secret key for password reset. + /// When the user clicks the confirmation link he is redirected back to your + /// app password reset URL with the secret key and email address values + /// attached to the URL query string. Use the query string params to submit a + /// request to the [PUT + /// /account/recovery](/docs/client/account#accountUpdateRecovery) endpoint to + /// complete the process. The verification link sent to the user's email + /// address is valid for 1 hour. + /// + Future<models.Token> createRecovery( + {required String email, required String url}) async { + const String path = '/account/recovery'; + + final Map<String, dynamic> params = { + 'email': email, + 'url': url, + }; + + final Map<String, String> headers = { + 'content-type': 'application/json', + }; + + final res = await client.call(CallParams( + HttpMethod.post, + path, + params: params, + headers: headers, + )); + + return models.Token.fromMap(res.data); + } + + /// Create Password Recovery (confirmation) + /// + /// Use this endpoint to complete the user account password reset. Both the + /// **userId** and **secret** arguments will be passed as query parameters to + /// the redirect URL you have provided when sending your request to the [POST + /// /account/recovery](/docs/client/account#accountCreateRecovery) endpoint. + /// + /// Please note that in order to avoid a [Redirect + /// Attack](https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) + /// the only valid redirect URLs are the ones from domains you have set when + /// adding your platforms in the console interface. + /// + Future<models.Token> updateRecovery( + {required String userId, + required String secret, + required String password, + required String passwordAgain}) async { + const String path = '/account/recovery'; + + final Map<String, dynamic> params = { + 'userId': userId, + 'secret': secret, + 'password': password, + 'passwordAgain': passwordAgain, + }; + + final Map<String, String> headers = { + 'content-type': 'application/json', + }; + + final res = await client.call(CallParams( + HttpMethod.put, + path, + params: params, + headers: headers, + )); + + return models.Token.fromMap(res.data); + } + + /// List Sessions + /// + /// Get currently logged in user list of active sessions across different + /// devices. + /// + Future<models.SessionList> listSessions() async { + const String path = '/account/sessions'; + + final Map<String, dynamic> params = {}; + + final Map<String, String> headers = { + 'content-type': 'application/json', + }; + + final res = await client.call(CallParams( + HttpMethod.get, + path, + params: params, + headers: headers, + )); + + return models.SessionList.fromMap(res.data); + } + + /// Delete Sessions + /// + /// Delete all sessions from the user account and remove any sessions cookies + /// from the end client. + /// + Future deleteSessions() async { + const String path = '/account/sessions'; + + final Map<String, dynamic> params = {}; + + final Map<String, String> headers = { + 'content-type': 'application/json', + }; + + final res = await client.call(CallParams( + HttpMethod.delete, + path, + params: params, + headers: headers, + )); + + return res.data; + } + + /// Create Anonymous Session + /// + /// Use this endpoint to allow a new user to register an anonymous account in + /// your project. This route will also create a new session for the user. To + /// allow the new user to convert an anonymous account to a normal account, you + /// need to update its [email and + /// password](/docs/client/account#accountUpdateEmail) or create an [OAuth2 + /// session](/docs/client/account#accountCreateOAuth2Session). + /// + Future<models.Session> createAnonymousSession() async { + const String path = '/account/sessions/anonymous'; + + final Map<String, dynamic> params = {}; + + final Map<String, String> headers = { + 'content-type': 'application/json', + }; + + final res = await client.call(CallParams( + HttpMethod.post, + path, + params: params, + headers: headers, + )); + + return models.Session.fromMap(res.data); + } + + /// Create Email Session + /// + /// Allow the user to login into their account by providing a valid email and + /// password combination. This route will create a new session for the user. + /// + /// A user is limited to 10 active sessions at a time by default. [Learn more + /// about session limits](/docs/authentication#limits). + /// + Future<models.Session> createEmailSession( + {required String email, required String password}) async { + const String path = '/account/sessions/email'; + + final Map<String, dynamic> params = { + 'email': email, + 'password': password, + }; + + final Map<String, String> headers = { + 'content-type': 'application/json', + }; + + final res = await client.call(CallParams( + HttpMethod.post, + path, + params: params, + headers: headers, + )); + + return models.Session.fromMap(res.data); + } + + /// Create Magic URL session + /// + /// Sends the user an email with a secret key for creating a session. If the + /// provided user ID has not be registered, a new user will be created. When + /// the user clicks the link in the email, the user is redirected back to the + /// URL you provided with the secret key and userId values attached to the URL + /// query string. Use the query string parameters to submit a request to the + /// [PUT + /// /account/sessions/magic-url](/docs/client/account#accountUpdateMagicURLSession) + /// endpoint to complete the login process. The link sent to the user's email + /// address is valid for 1 hour. If you are on a mobile device you can leave + /// the URL parameter empty, so that the login completion will be handled by + /// your Appwrite instance by default. + /// + /// A user is limited to 10 active sessions at a time by default. [Learn more + /// about session limits](/docs/authentication#limits). + /// + Future<models.Token> createMagicURLSession( + {required String userId, required String email, String? url}) async { + const String path = '/account/sessions/magic-url'; + + final Map<String, dynamic> params = { + 'userId': userId, + 'email': email, + 'url': url, + }; + + final Map<String, String> headers = { + 'content-type': 'application/json', + }; + + final res = await client.call(CallParams( + HttpMethod.post, + path, + params: params, + headers: headers, + )); + + return models.Token.fromMap(res.data); + } + + /// Create Magic URL session (confirmation) + /// + /// Use this endpoint to complete creating the session with the Magic URL. Both + /// the **userId** and **secret** arguments will be passed as query parameters + /// to the redirect URL you have provided when sending your request to the + /// [POST + /// /account/sessions/magic-url](/docs/client/account#accountCreateMagicURLSession) + /// endpoint. + /// + /// Please note that in order to avoid a [Redirect + /// Attack](https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) + /// the only valid redirect URLs are the ones from domains you have set when + /// adding your platforms in the console interface. + /// + Future<models.Session> updateMagicURLSession( + {required String userId, required String secret}) async { + const String path = '/account/sessions/magic-url'; + + final Map<String, dynamic> params = { + 'userId': userId, + 'secret': secret, + }; + + final Map<String, String> headers = { + 'content-type': 'application/json', + }; + + final res = await client.call(CallParams( + HttpMethod.put, + path, + params: params, + headers: headers, + )); + + return models.Session.fromMap(res.data); + } + + /// Create OAuth2 Session + /// + /// Allow the user to login to their account using the OAuth2 provider of their + /// choice. Each OAuth2 provider should be enabled from the Appwrite console + /// first. Use the success and failure arguments to provide a redirect URL's + /// back to your app when login is completed. + /// + /// If there is already an active session, the new session will be attached to + /// the logged-in account. If there are no active sessions, the server will + /// attempt to look for a user with the same email address as the email + /// received from the OAuth2 provider and attach the new session to the + /// existing user. If no matching user is found - the server will create a new + /// user. + /// + /// A user is limited to 10 active sessions at a time by default. [Learn more + /// about session limits](/docs/authentication#limits). + /// + /// + Future createOAuth2Session( + {required String provider, + String? success, + String? failure, + List<String>? scopes}) async { + final String path = '/account/sessions/oauth2/{provider}' + .replaceAll('{provider}', provider); + + final Map<String, dynamic> params = { + 'success': success, + 'failure': failure, + 'scopes': scopes, + 'project': client.config['project'], + }; + + final List query = []; + + params.forEach((key, value) { + if (value is List) { + for (var item in value) { + query.add(Uri.encodeComponent(key + '[]') + + '=' + + Uri.encodeComponent(item)); + } + } else if (value != null) { + query.add(Uri.encodeComponent(key) + '=' + Uri.encodeComponent(value)); + } + }); + + Uri endpoint = Uri.parse(client.endPoint); + Uri url = Uri( + scheme: endpoint.scheme, + host: endpoint.host, + port: endpoint.port, + path: endpoint.path + path, + query: query.join('&')); + + return client.webAuth(url, callbackUrlScheme: success); + } + + /// Create Phone session + /// + /// Sends the user an SMS with a secret key for creating a session. If the + /// provided user ID has not be registered, a new user will be created. Use the + /// returned user ID and secret and submit a request to the [PUT + /// /account/sessions/phone](/docs/client/account#accountUpdatePhoneSession) + /// endpoint to complete the login process. The secret sent to the user's phone + /// is valid for 15 minutes. + /// + /// A user is limited to 10 active sessions at a time by default. [Learn more + /// about session limits](/docs/authentication#limits). + /// + Future<models.Token> createPhoneSession( + {required String userId, required String phone}) async { + const String path = '/account/sessions/phone'; + + final Map<String, dynamic> params = { + 'userId': userId, + 'phone': phone, + }; + + final Map<String, String> headers = { + 'content-type': 'application/json', + }; + + final res = await client.call(CallParams( + HttpMethod.post, + path, + params: params, + headers: headers, + )); + + return models.Token.fromMap(res.data); + } + + /// Create Phone Session (confirmation) + /// + /// Use this endpoint to complete creating a session with SMS. Use the + /// **userId** from the + /// [createPhoneSession](/docs/client/account#accountCreatePhoneSession) + /// endpoint and the **secret** received via SMS to successfully update and + /// confirm the phone session. + /// + Future<models.Session> updatePhoneSession( + {required String userId, required String secret}) async { + const String path = '/account/sessions/phone'; + + final Map<String, dynamic> params = { + 'userId': userId, + 'secret': secret, + }; + + final Map<String, String> headers = { + 'content-type': 'application/json', + }; + + final res = await client.call(CallParams( + HttpMethod.put, + path, + params: params, + headers: headers, + )); + + return models.Session.fromMap(res.data); + } + + /// Get Session + /// + /// Use this endpoint to get a logged in user's session using a Session ID. + /// Inputting 'current' will return the current session being used. + /// + Future<models.Session> getSession({required String sessionId}) async { + final String path = + '/account/sessions/{sessionId}'.replaceAll('{sessionId}', sessionId); + + final Map<String, dynamic> params = {}; + + final Map<String, String> headers = { + 'content-type': 'application/json', + }; + + final res = await client.call(CallParams( + HttpMethod.get, + path, + params: params, + headers: headers, + )); + + return models.Session.fromMap(res.data); + } + + /// Update OAuth Session (Refresh Tokens) + /// + /// Access tokens have limited lifespan and expire to mitigate security risks. + /// If session was created using an OAuth provider, this route can be used to + /// "refresh" the access token. + /// + Future<models.Session> updateSession({required String sessionId}) async { + final String path = + '/account/sessions/{sessionId}'.replaceAll('{sessionId}', sessionId); + + final Map<String, dynamic> params = {}; + + final Map<String, String> headers = { + 'content-type': 'application/json', + }; + + final res = await client.call(CallParams( + HttpMethod.patch, + path, + params: params, + headers: headers, + )); + + return models.Session.fromMap(res.data); + } + + /// Delete Session + /// + /// Use this endpoint to log out the currently logged in user from all their + /// account sessions across all of their different devices. When using the + /// Session ID argument, only the unique session ID provided is deleted. + /// + /// + Future deleteSession({required String sessionId}) async { + final String path = + '/account/sessions/{sessionId}'.replaceAll('{sessionId}', sessionId); + + final Map<String, dynamic> params = {}; + + final Map<String, String> headers = { + 'content-type': 'application/json', + }; + + final res = await client.call(CallParams( + HttpMethod.delete, + path, + params: params, + headers: headers, + )); + + return res.data; + } + + /// Update Status + /// + /// Block the currently logged in user account. Behind the scene, the user + /// record is not deleted but permanently blocked from any access. To + /// completely delete a user, use the Users API instead. + /// + Future<models.Account> updateStatus() async { + const String path = '/account/status'; + + final Map<String, dynamic> params = {}; + + final Map<String, String> headers = { + 'content-type': 'application/json', + }; + + final res = await client.call(CallParams( + HttpMethod.patch, + path, + params: params, + headers: headers, + )); + + return models.Account.fromMap(res.data); + } + + /// Create Email Verification + /// + /// Use this endpoint to send a verification message to your user email address + /// to confirm they are the valid owners of that address. Both the **userId** + /// and **secret** arguments will be passed as query parameters to the URL you + /// have provided to be attached to the verification email. The provided URL + /// should redirect the user back to your app and allow you to complete the + /// verification process by verifying both the **userId** and **secret** + /// parameters. Learn more about how to [complete the verification + /// process](/docs/client/account#accountUpdateEmailVerification). The + /// verification link sent to the user's email address is valid for 7 days. + /// + /// Please note that in order to avoid a [Redirect + /// Attack](https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md), + /// the only valid redirect URLs are the ones from domains you have set when + /// adding your platforms in the console interface. + /// + /// + Future<models.Token> createVerification({required String url}) async { + const String path = '/account/verification'; + + final Map<String, dynamic> params = { + 'url': url, + }; + + final Map<String, String> headers = { + 'content-type': 'application/json', + }; + + final res = await client.call(CallParams( + HttpMethod.post, + path, + params: params, + headers: headers, + )); + + return models.Token.fromMap(res.data); + } + + /// Create Email Verification (confirmation) + /// + /// Use this endpoint to complete the user email verification process. Use both + /// the **userId** and **secret** parameters that were attached to your app URL + /// to verify the user email ownership. If confirmed this route will return a + /// 200 status code. + /// + Future<models.Token> updateVerification( + {required String userId, required String secret}) async { + const String path = '/account/verification'; + + final Map<String, dynamic> params = { + 'userId': userId, + 'secret': secret, + }; + + final Map<String, String> headers = { + 'content-type': 'application/json', + }; + + final res = await client.call(CallParams( + HttpMethod.put, + path, + params: params, + headers: headers, + )); + + return models.Token.fromMap(res.data); + } + + /// Create Phone Verification + /// + /// Use this endpoint to send a verification SMS to the currently logged in + /// user. This endpoint is meant for use after updating a user's phone number + /// using the [accountUpdatePhone](/docs/client/account#accountUpdatePhone) + /// endpoint. Learn more about how to [complete the verification + /// process](/docs/client/account#accountUpdatePhoneVerification). The + /// verification code sent to the user's phone number is valid for 15 minutes. + /// + Future<models.Token> createPhoneVerification() async { + const String path = '/account/verification/phone'; + + final Map<String, dynamic> params = {}; + + final Map<String, String> headers = { + 'content-type': 'application/json', + }; + + final res = await client.call(CallParams( + HttpMethod.post, + path, + params: params, + headers: headers, + )); + + return models.Token.fromMap(res.data); + } + + /// Create Phone Verification (confirmation) + /// + /// Use this endpoint to complete the user phone verification process. Use the + /// **userId** and **secret** that were sent to your user's phone number to + /// verify the user email ownership. If confirmed this route will return a 200 + /// status code. + /// + Future<models.Token> updatePhoneVerification( + {required String userId, required String secret}) async { + const String path = '/account/verification/phone'; + + final Map<String, dynamic> params = { + 'userId': userId, + 'secret': secret, + }; + + final Map<String, String> headers = { + 'content-type': 'application/json', + }; + + final res = await client.call(CallParams( + HttpMethod.put, + path, + params: params, + headers: headers, + )); + + return models.Token.fromMap(res.data); + } +} diff --git a/lib/services/avatars.dart b/lib/services/avatars.dart index 6962ff5f..3d35e9fa 100644 --- a/lib/services/avatars.dart +++ b/lib/services/avatars.dart @@ -1,200 +1,228 @@ part of appwrite; - /// The Avatars service aims to help you complete everyday tasks related to - /// your app image, icons, and avatars. +/// The Avatars service aims to help you complete everyday tasks related to +/// your app image, icons, and avatars. class Avatars extends Service { - Avatars(super.client); - - /// Get Browser Icon - /// - /// You can use this endpoint to show different browser icons to your users. - /// The code argument receives the browser code as it appears in your user [GET - /// /account/sessions](/docs/client/account#accountGetSessions) endpoint. Use - /// width, height and quality arguments to change the output settings. - /// - /// When one dimension is specified and the other is 0, the image is scaled - /// with preserved aspect ratio. If both dimensions are 0, the API provides an - /// image at source quality. If dimensions are not specified, the default size - /// of image returned is 100x100px. - /// - Future<Uint8List> getBrowser({required String code, int? width, int? height, int? quality}) async { - final String path = '/avatars/browsers/{code}'.replaceAll('{code}', code); - - final Map<String, dynamic> params = { - - 'width': width, - 'height': height, - 'quality': quality, - - 'project': client.config['project'], - }; - - final res = await client.call(HttpMethod.get, path: path, params: params, responseType: ResponseType.bytes); - return res.data; - } - - /// Get Credit Card Icon - /// - /// The credit card endpoint will return you the icon of the credit card - /// provider you need. Use width, height and quality arguments to change the - /// output settings. - /// - /// When one dimension is specified and the other is 0, the image is scaled - /// with preserved aspect ratio. If both dimensions are 0, the API provides an - /// image at source quality. If dimensions are not specified, the default size - /// of image returned is 100x100px. - /// - /// - Future<Uint8List> getCreditCard({required String code, int? width, int? height, int? quality}) async { - final String path = '/avatars/credit-cards/{code}'.replaceAll('{code}', code); - - final Map<String, dynamic> params = { - - 'width': width, - 'height': height, - 'quality': quality, - - 'project': client.config['project'], - }; - - final res = await client.call(HttpMethod.get, path: path, params: params, responseType: ResponseType.bytes); - return res.data; - } - - /// Get Favicon - /// - /// Use this endpoint to fetch the favorite icon (AKA favicon) of any remote - /// website URL. - /// - /// - Future<Uint8List> getFavicon({required String url}) async { - const String path = '/avatars/favicon'; - - final Map<String, dynamic> params = { - - 'url': url, - - 'project': client.config['project'], - }; - - final res = await client.call(HttpMethod.get, path: path, params: params, responseType: ResponseType.bytes); - return res.data; - } - - /// Get Country Flag - /// - /// You can use this endpoint to show different country flags icons to your - /// users. The code argument receives the 2 letter country code. Use width, - /// height and quality arguments to change the output settings. Country codes - /// follow the [ISO 3166-1](http://en.wikipedia.org/wiki/ISO_3166-1) standard. - /// - /// When one dimension is specified and the other is 0, the image is scaled - /// with preserved aspect ratio. If both dimensions are 0, the API provides an - /// image at source quality. If dimensions are not specified, the default size - /// of image returned is 100x100px. - /// - /// - Future<Uint8List> getFlag({required String code, int? width, int? height, int? quality}) async { - final String path = '/avatars/flags/{code}'.replaceAll('{code}', code); - - final Map<String, dynamic> params = { - - 'width': width, - 'height': height, - 'quality': quality, - - 'project': client.config['project'], - }; - - final res = await client.call(HttpMethod.get, path: path, params: params, responseType: ResponseType.bytes); - return res.data; - } - - /// Get Image from URL - /// - /// Use this endpoint to fetch a remote image URL and crop it to any image size - /// you want. This endpoint is very useful if you need to crop and display - /// remote images in your app or in case you want to make sure a 3rd party - /// image is properly served using a TLS protocol. - /// - /// When one dimension is specified and the other is 0, the image is scaled - /// with preserved aspect ratio. If both dimensions are 0, the API provides an - /// image at source quality. If dimensions are not specified, the default size - /// of image returned is 400x400px. - /// - /// - Future<Uint8List> getImage({required String url, int? width, int? height}) async { - const String path = '/avatars/image'; - - final Map<String, dynamic> params = { - - 'url': url, - 'width': width, - 'height': height, - - 'project': client.config['project'], - }; - - final res = await client.call(HttpMethod.get, path: path, params: params, responseType: ResponseType.bytes); - return res.data; - } - - /// Get User Initials - /// - /// Use this endpoint to show your user initials avatar icon on your website or - /// app. By default, this route will try to print your logged-in user name or - /// email initials. You can also overwrite the user name if you pass the 'name' - /// parameter. If no name is given and no user is logged, an empty avatar will - /// be returned. - /// - /// You can use the color and background params to change the avatar colors. By - /// default, a random theme will be selected. The random theme will persist for - /// the user's initials when reloading the same theme will always return for - /// the same initials. - /// - /// When one dimension is specified and the other is 0, the image is scaled - /// with preserved aspect ratio. If both dimensions are 0, the API provides an - /// image at source quality. If dimensions are not specified, the default size - /// of image returned is 100x100px. - /// - /// - Future<Uint8List> getInitials({String? name, int? width, int? height, String? background}) async { - const String path = '/avatars/initials'; - - final Map<String, dynamic> params = { - - 'name': name, - 'width': width, - 'height': height, - 'background': background, - - 'project': client.config['project'], - }; - - final res = await client.call(HttpMethod.get, path: path, params: params, responseType: ResponseType.bytes); - return res.data; - } - - /// Get QR Code - /// - /// Converts a given plain text to a QR code image. You can use the query - /// parameters to change the size and style of the resulting image. - /// - /// - Future<Uint8List> getQR({required String text, int? size, int? margin, bool? download}) async { - const String path = '/avatars/qr'; - - final Map<String, dynamic> params = { - - 'text': text, - 'size': size, - 'margin': margin, - 'download': download, - - 'project': client.config['project'], - }; - - final res = await client.call(HttpMethod.get, path: path, params: params, responseType: ResponseType.bytes); - return res.data; - } -} \ No newline at end of file + Avatars(super.client); + + /// Get Browser Icon + /// + /// You can use this endpoint to show different browser icons to your users. + /// The code argument receives the browser code as it appears in your user [GET + /// /account/sessions](/docs/client/account#accountGetSessions) endpoint. Use + /// width, height and quality arguments to change the output settings. + /// + /// When one dimension is specified and the other is 0, the image is scaled + /// with preserved aspect ratio. If both dimensions are 0, the API provides an + /// image at source quality. If dimensions are not specified, the default size + /// of image returned is 100x100px. + /// + Future<Uint8List> getBrowser( + {required String code, int? width, int? height, int? quality}) async { + final String path = '/avatars/browsers/{code}'.replaceAll('{code}', code); + + final Map<String, dynamic> params = { + 'width': width, + 'height': height, + 'quality': quality, + 'project': client.config['project'], + }; + + final res = await client.call(CallParams( + HttpMethod.get, + path, + params: params, + responseType: ResponseType.bytes, + )); + return res.data; + } + + /// Get Credit Card Icon + /// + /// The credit card endpoint will return you the icon of the credit card + /// provider you need. Use width, height and quality arguments to change the + /// output settings. + /// + /// When one dimension is specified and the other is 0, the image is scaled + /// with preserved aspect ratio. If both dimensions are 0, the API provides an + /// image at source quality. If dimensions are not specified, the default size + /// of image returned is 100x100px. + /// + /// + Future<Uint8List> getCreditCard( + {required String code, int? width, int? height, int? quality}) async { + final String path = + '/avatars/credit-cards/{code}'.replaceAll('{code}', code); + + final Map<String, dynamic> params = { + 'width': width, + 'height': height, + 'quality': quality, + 'project': client.config['project'], + }; + + final res = await client.call(CallParams( + HttpMethod.get, + path, + params: params, + responseType: ResponseType.bytes, + )); + return res.data; + } + + /// Get Favicon + /// + /// Use this endpoint to fetch the favorite icon (AKA favicon) of any remote + /// website URL. + /// + /// + Future<Uint8List> getFavicon({required String url}) async { + const String path = '/avatars/favicon'; + + final Map<String, dynamic> params = { + 'url': url, + 'project': client.config['project'], + }; + + final res = await client.call(CallParams( + HttpMethod.get, + path, + params: params, + responseType: ResponseType.bytes, + )); + return res.data; + } + + /// Get Country Flag + /// + /// You can use this endpoint to show different country flags icons to your + /// users. The code argument receives the 2 letter country code. Use width, + /// height and quality arguments to change the output settings. Country codes + /// follow the [ISO 3166-1](http://en.wikipedia.org/wiki/ISO_3166-1) standard. + /// + /// When one dimension is specified and the other is 0, the image is scaled + /// with preserved aspect ratio. If both dimensions are 0, the API provides an + /// image at source quality. If dimensions are not specified, the default size + /// of image returned is 100x100px. + /// + /// + Future<Uint8List> getFlag( + {required String code, int? width, int? height, int? quality}) async { + final String path = '/avatars/flags/{code}'.replaceAll('{code}', code); + + final Map<String, dynamic> params = { + 'width': width, + 'height': height, + 'quality': quality, + 'project': client.config['project'], + }; + + final res = await client.call(CallParams( + HttpMethod.get, + path, + params: params, + responseType: ResponseType.bytes, + )); + return res.data; + } + + /// Get Image from URL + /// + /// Use this endpoint to fetch a remote image URL and crop it to any image size + /// you want. This endpoint is very useful if you need to crop and display + /// remote images in your app or in case you want to make sure a 3rd party + /// image is properly served using a TLS protocol. + /// + /// When one dimension is specified and the other is 0, the image is scaled + /// with preserved aspect ratio. If both dimensions are 0, the API provides an + /// image at source quality. If dimensions are not specified, the default size + /// of image returned is 400x400px. + /// + /// + Future<Uint8List> getImage( + {required String url, int? width, int? height}) async { + const String path = '/avatars/image'; + + final Map<String, dynamic> params = { + 'url': url, + 'width': width, + 'height': height, + 'project': client.config['project'], + }; + + final res = await client.call(CallParams( + HttpMethod.get, + path, + params: params, + responseType: ResponseType.bytes, + )); + return res.data; + } + + /// Get User Initials + /// + /// Use this endpoint to show your user initials avatar icon on your website or + /// app. By default, this route will try to print your logged-in user name or + /// email initials. You can also overwrite the user name if you pass the 'name' + /// parameter. If no name is given and no user is logged, an empty avatar will + /// be returned. + /// + /// You can use the color and background params to change the avatar colors. By + /// default, a random theme will be selected. The random theme will persist for + /// the user's initials when reloading the same theme will always return for + /// the same initials. + /// + /// When one dimension is specified and the other is 0, the image is scaled + /// with preserved aspect ratio. If both dimensions are 0, the API provides an + /// image at source quality. If dimensions are not specified, the default size + /// of image returned is 100x100px. + /// + /// + Future<Uint8List> getInitials( + {String? name, int? width, int? height, String? background}) async { + const String path = '/avatars/initials'; + + final Map<String, dynamic> params = { + 'name': name, + 'width': width, + 'height': height, + 'background': background, + 'project': client.config['project'], + }; + + final res = await client.call(CallParams( + HttpMethod.get, + path, + params: params, + responseType: ResponseType.bytes, + )); + return res.data; + } + + /// Get QR Code + /// + /// Converts a given plain text to a QR code image. You can use the query + /// parameters to change the size and style of the resulting image. + /// + /// + Future<Uint8List> getQR( + {required String text, int? size, int? margin, bool? download}) async { + const String path = '/avatars/qr'; + + final Map<String, dynamic> params = { + 'text': text, + 'size': size, + 'margin': margin, + 'download': download, + 'project': client.config['project'], + }; + + final res = await client.call(CallParams( + HttpMethod.get, + path, + params: params, + responseType: ResponseType.bytes, + )); + return res.data; + } +} diff --git a/lib/services/databases.dart b/lib/services/databases.dart index d33661a1..a1c03f3d 100644 --- a/lib/services/databases.dart +++ b/lib/services/databases.dart @@ -1,189 +1,174 @@ part of appwrite; - /// The Databases service allows you to create structured collections of - /// documents, query and filter lists of documents +/// The Databases service allows you to create structured collections of +/// documents, query and filter lists of documents class Databases extends Service { - Databases(super.client); - - /// List Documents - /// - /// Get a list of all the user's documents in a given collection. You can use - /// the query params to filter your results. - /// - Future<models.DocumentList> listDocuments({required String databaseId, required String collectionId, List<String>? queries}) async { - final String path = '/databases/{databaseId}/collections/{collectionId}/documents'.replaceAll('{databaseId}', databaseId).replaceAll('{collectionId}', collectionId); - - final Map<String, dynamic> params = { - 'queries': queries, - }; - - final Map<String, String> headers = { - 'content-type': 'application/json', - }; - - final cacheModel = '/databases/{databaseId}/collections/{collectionId}/documents'.replaceAll('{databaseId}', databaseId).replaceAll('{collectionId}', collectionId); - final cacheKey = ''; - final cacheResponseIdKey = '\$id'; - final cacheResponseContainerKey = 'documents'; - - final res = await client.call( - HttpMethod.get, - path: path, - params: params, - headers: headers, - cacheModel: cacheModel, - cacheKey: cacheKey, - cacheResponseIdKey: cacheResponseIdKey, - cacheResponseContainerKey: cacheResponseContainerKey, - ); - - return models.DocumentList.fromMap(res.data); - - } - - /// Create Document - /// - /// Create a new Document. Before using this route, you should create a new - /// collection resource using either a [server - /// integration](/docs/server/databases#databasesCreateCollection) API or - /// directly from your database console. - /// - Future<models.Document> createDocument({required String databaseId, required String collectionId, required String documentId, required Map data, List<String>? permissions}) async { - final String path = '/databases/{databaseId}/collections/{collectionId}/documents'.replaceAll('{databaseId}', databaseId).replaceAll('{collectionId}', collectionId); - - final Map<String, dynamic> params = { - 'documentId': documentId, - 'data': data, - 'permissions': permissions, - }; - - final Map<String, String> headers = { - 'content-type': 'application/json', - }; - - final cacheModel = '/databases/{databaseId}/collections/{collectionId}/documents'.replaceAll('{databaseId}', databaseId).replaceAll('{collectionId}', collectionId); - final cacheKey = documentId; - final cacheResponseIdKey = '\$id'; - final cacheResponseContainerKey = ''; - - final res = await client.call( - HttpMethod.post, - path: path, - params: params, - headers: headers, - cacheModel: cacheModel, - cacheKey: cacheKey, - cacheResponseIdKey: cacheResponseIdKey, - cacheResponseContainerKey: cacheResponseContainerKey, - ); - - return models.Document.fromMap(res.data); - - } - - /// Get Document - /// - /// Get a document by its unique ID. This endpoint response returns a JSON - /// object with the document data. - /// - Future<models.Document> getDocument({required String databaseId, required String collectionId, required String documentId}) async { - final String path = '/databases/{databaseId}/collections/{collectionId}/documents/{documentId}'.replaceAll('{databaseId}', databaseId).replaceAll('{collectionId}', collectionId).replaceAll('{documentId}', documentId); - - final Map<String, dynamic> params = { - }; - - final Map<String, String> headers = { - 'content-type': 'application/json', - }; - - final cacheModel = '/databases/{databaseId}/collections/{collectionId}/documents'.replaceAll('{databaseId}', databaseId).replaceAll('{collectionId}', collectionId).replaceAll('{documentId}', documentId); - final cacheKey = documentId; - final cacheResponseIdKey = '\$id'; - final cacheResponseContainerKey = ''; - - final res = await client.call( - HttpMethod.get, - path: path, - params: params, - headers: headers, - cacheModel: cacheModel, - cacheKey: cacheKey, - cacheResponseIdKey: cacheResponseIdKey, - cacheResponseContainerKey: cacheResponseContainerKey, - ); - - return models.Document.fromMap(res.data); - - } - - /// Update Document - /// - /// Update a document by its unique ID. Using the patch method you can pass - /// only specific fields that will get updated. - /// - Future<models.Document> updateDocument({required String databaseId, required String collectionId, required String documentId, Map? data, List<String>? permissions}) async { - final String path = '/databases/{databaseId}/collections/{collectionId}/documents/{documentId}'.replaceAll('{databaseId}', databaseId).replaceAll('{collectionId}', collectionId).replaceAll('{documentId}', documentId); - - final Map<String, dynamic> params = { - 'data': data, - 'permissions': permissions, - }; - - final Map<String, String> headers = { - 'content-type': 'application/json', - }; - - final cacheModel = '/databases/{databaseId}/collections/{collectionId}/documents'.replaceAll('{databaseId}', databaseId).replaceAll('{collectionId}', collectionId).replaceAll('{documentId}', documentId); - final cacheKey = documentId; - final cacheResponseIdKey = '\$id'; - final cacheResponseContainerKey = ''; - - final res = await client.call( - HttpMethod.patch, - path: path, - params: params, - headers: headers, - cacheModel: cacheModel, - cacheKey: cacheKey, - cacheResponseIdKey: cacheResponseIdKey, - cacheResponseContainerKey: cacheResponseContainerKey, - ); - - return models.Document.fromMap(res.data); - - } - - /// Delete Document - /// - /// Delete a document by its unique ID. - /// - Future deleteDocument({required String databaseId, required String collectionId, required String documentId}) async { - final String path = '/databases/{databaseId}/collections/{collectionId}/documents/{documentId}'.replaceAll('{databaseId}', databaseId).replaceAll('{collectionId}', collectionId).replaceAll('{documentId}', documentId); - - final Map<String, dynamic> params = { - }; - - final Map<String, String> headers = { - 'content-type': 'application/json', - }; - - final cacheModel = '/databases/{databaseId}/collections/{collectionId}/documents'.replaceAll('{databaseId}', databaseId).replaceAll('{collectionId}', collectionId).replaceAll('{documentId}', documentId); - final cacheKey = documentId; - final cacheResponseIdKey = '\$id'; - final cacheResponseContainerKey = ''; - - final res = await client.call( - HttpMethod.delete, - path: path, - params: params, - headers: headers, - cacheModel: cacheModel, - cacheKey: cacheKey, - cacheResponseIdKey: cacheResponseIdKey, - cacheResponseContainerKey: cacheResponseContainerKey, - ); - - return res.data; - - } -} \ No newline at end of file + Databases(super.client); + + /// List Documents + /// + /// Get a list of all the user's documents in a given collection. You can use + /// the query params to filter your results. + /// + Future<models.DocumentList> listDocuments( + {required String databaseId, + required String collectionId, + List<String>? queries}) async { + final String path = + '/databases/{databaseId}/collections/{collectionId}/documents' + .replaceAll('{databaseId}', databaseId) + .replaceAll('{collectionId}', collectionId); + + final Map<String, dynamic> params = { + 'queries': queries, + }; + + final Map<String, String> headers = { + 'content-type': 'application/json', + }; + + final res = await client.call(CallParams( + HttpMethod.get, + path, + params: params, + headers: headers, + )); + + return models.DocumentList.fromMap(res.data); + } + + /// Create Document + /// + /// Create a new Document. Before using this route, you should create a new + /// collection resource using either a [server + /// integration](/docs/server/databases#databasesCreateCollection) API or + /// directly from your database console. + /// + Future<models.Document> createDocument( + {required String databaseId, + required String collectionId, + required String documentId, + required Map data, + List<String>? permissions}) async { + final String path = + '/databases/{databaseId}/collections/{collectionId}/documents' + .replaceAll('{databaseId}', databaseId) + .replaceAll('{collectionId}', collectionId); + + final Map<String, dynamic> params = { + 'documentId': documentId, + 'data': data, + 'permissions': permissions, + }; + + final Map<String, String> headers = { + 'content-type': 'application/json', + }; + + final res = await client.call(CallParams( + HttpMethod.post, + path, + params: params, + headers: headers, + )); + + return models.Document.fromMap(res.data); + } + + /// Get Document + /// + /// Get a document by its unique ID. This endpoint response returns a JSON + /// object with the document data. + /// + Future<models.Document> getDocument( + {required String databaseId, + required String collectionId, + required String documentId}) async { + final String path = + '/databases/{databaseId}/collections/{collectionId}/documents/{documentId}' + .replaceAll('{databaseId}', databaseId) + .replaceAll('{collectionId}', collectionId) + .replaceAll('{documentId}', documentId); + + final Map<String, dynamic> params = {}; + + final Map<String, String> headers = { + 'content-type': 'application/json', + }; + + final res = await client.call(CallParams( + HttpMethod.get, + path, + params: params, + headers: headers, + )); + + return models.Document.fromMap(res.data); + } + + /// Update Document + /// + /// Update a document by its unique ID. Using the patch method you can pass + /// only specific fields that will get updated. + /// + Future<models.Document> updateDocument( + {required String databaseId, + required String collectionId, + required String documentId, + Map? data, + List<String>? permissions}) async { + final String path = + '/databases/{databaseId}/collections/{collectionId}/documents/{documentId}' + .replaceAll('{databaseId}', databaseId) + .replaceAll('{collectionId}', collectionId) + .replaceAll('{documentId}', documentId); + + final Map<String, dynamic> params = { + 'data': data, + 'permissions': permissions, + }; + + final Map<String, String> headers = { + 'content-type': 'application/json', + }; + + final res = await client.call(CallParams( + HttpMethod.patch, + path, + params: params, + headers: headers, + )); + + return models.Document.fromMap(res.data); + } + + /// Delete Document + /// + /// Delete a document by its unique ID. + /// + Future deleteDocument( + {required String databaseId, + required String collectionId, + required String documentId}) async { + final String path = + '/databases/{databaseId}/collections/{collectionId}/documents/{documentId}' + .replaceAll('{databaseId}', databaseId) + .replaceAll('{collectionId}', collectionId) + .replaceAll('{documentId}', documentId); + + final Map<String, dynamic> params = {}; + + final Map<String, String> headers = { + 'content-type': 'application/json', + }; + + final res = await client.call(CallParams( + HttpMethod.delete, + path, + params: params, + headers: headers, + )); + + return res.data; + } +} diff --git a/lib/services/functions.dart b/lib/services/functions.dart index 1810f1b5..37afdc2d 100644 --- a/lib/services/functions.dart +++ b/lib/services/functions.dart @@ -1,117 +1,95 @@ part of appwrite; - /// The Functions Service allows you view, create and manage your Cloud - /// Functions. +/// The Functions Service allows you view, create and manage your Cloud +/// Functions. class Functions extends Service { - Functions(super.client); - - /// List Executions - /// - /// Get a list of all the current user function execution logs. You can use the - /// query params to filter your results. - /// - Future<models.ExecutionList> listExecutions({required String functionId, List<String>? queries, String? search}) async { - final String path = '/functions/{functionId}/executions'.replaceAll('{functionId}', functionId); - - final Map<String, dynamic> params = { - 'queries': queries, - 'search': search, - }; - - final Map<String, String> headers = { - 'content-type': 'application/json', - }; - - final cacheModel = ''; - final cacheKey = ''; - final cacheResponseIdKey = '\$id'; - final cacheResponseContainerKey = 'executions'; - - final res = await client.call( - HttpMethod.get, - path: path, - params: params, - headers: headers, - cacheModel: cacheModel, - cacheKey: cacheKey, - cacheResponseIdKey: cacheResponseIdKey, - cacheResponseContainerKey: cacheResponseContainerKey, - ); - - return models.ExecutionList.fromMap(res.data); - - } - - /// Create Execution - /// - /// Trigger a function execution. The returned object will return you the - /// current execution status. You can ping the `Get Execution` endpoint to get - /// updates on the current execution status. Once this endpoint is called, your - /// function execution process will start asynchronously. - /// - Future<models.Execution> createExecution({required String functionId, String? data, bool? xasync}) async { - final String path = '/functions/{functionId}/executions'.replaceAll('{functionId}', functionId); - - final Map<String, dynamic> params = { - 'data': data, - 'async': xasync, - }; - - final Map<String, String> headers = { - 'content-type': 'application/json', - }; - - final cacheModel = ''; - final cacheKey = ''; - final cacheResponseIdKey = '\$id'; - final cacheResponseContainerKey = ''; - - final res = await client.call( - HttpMethod.post, - path: path, - params: params, - headers: headers, - cacheModel: cacheModel, - cacheKey: cacheKey, - cacheResponseIdKey: cacheResponseIdKey, - cacheResponseContainerKey: cacheResponseContainerKey, - ); - - return models.Execution.fromMap(res.data); - - } - - /// Get Execution - /// - /// Get a function execution log by its unique ID. - /// - Future<models.Execution> getExecution({required String functionId, required String executionId}) async { - final String path = '/functions/{functionId}/executions/{executionId}'.replaceAll('{functionId}', functionId).replaceAll('{executionId}', executionId); - - final Map<String, dynamic> params = { - }; - - final Map<String, String> headers = { - 'content-type': 'application/json', - }; - - final cacheModel = ''; - final cacheKey = ''; - final cacheResponseIdKey = '\$id'; - final cacheResponseContainerKey = ''; - - final res = await client.call( - HttpMethod.get, - path: path, - params: params, - headers: headers, - cacheModel: cacheModel, - cacheKey: cacheKey, - cacheResponseIdKey: cacheResponseIdKey, - cacheResponseContainerKey: cacheResponseContainerKey, - ); - - return models.Execution.fromMap(res.data); - - } -} \ No newline at end of file + Functions(super.client); + + /// List Executions + /// + /// Get a list of all the current user function execution logs. You can use the + /// query params to filter your results. + /// + Future<models.ExecutionList> listExecutions( + {required String functionId, + List<String>? queries, + String? search}) async { + final String path = '/functions/{functionId}/executions' + .replaceAll('{functionId}', functionId); + + final Map<String, dynamic> params = { + 'queries': queries, + 'search': search, + }; + + final Map<String, String> headers = { + 'content-type': 'application/json', + }; + + final res = await client.call(CallParams( + HttpMethod.get, + path, + params: params, + headers: headers, + )); + + return models.ExecutionList.fromMap(res.data); + } + + /// Create Execution + /// + /// Trigger a function execution. The returned object will return you the + /// current execution status. You can ping the `Get Execution` endpoint to get + /// updates on the current execution status. Once this endpoint is called, your + /// function execution process will start asynchronously. + /// + Future<models.Execution> createExecution( + {required String functionId, String? data, bool? xasync}) async { + final String path = '/functions/{functionId}/executions' + .replaceAll('{functionId}', functionId); + + final Map<String, dynamic> params = { + 'data': data, + 'async': xasync, + }; + + final Map<String, String> headers = { + 'content-type': 'application/json', + }; + + final res = await client.call(CallParams( + HttpMethod.post, + path, + params: params, + headers: headers, + )); + + return models.Execution.fromMap(res.data); + } + + /// Get Execution + /// + /// Get a function execution log by its unique ID. + /// + Future<models.Execution> getExecution( + {required String functionId, required String executionId}) async { + final String path = '/functions/{functionId}/executions/{executionId}' + .replaceAll('{functionId}', functionId) + .replaceAll('{executionId}', executionId); + + final Map<String, dynamic> params = {}; + + final Map<String, String> headers = { + 'content-type': 'application/json', + }; + + final res = await client.call(CallParams( + HttpMethod.get, + path, + params: params, + headers: headers, + )); + + return models.Execution.fromMap(res.data); + } +} diff --git a/lib/services/graphql.dart b/lib/services/graphql.dart index 2f92eaf1..96804d88 100644 --- a/lib/services/graphql.dart +++ b/lib/services/graphql.dart @@ -1,77 +1,59 @@ part of appwrite; - /// The GraphQL API allows you to query and mutate your Appwrite server using - /// GraphQL. +/// The GraphQL API allows you to query and mutate your Appwrite server using +/// GraphQL. class Graphql extends Service { - Graphql(super.client); - - /// GraphQL Endpoint - /// - /// Execute a GraphQL mutation. - /// - Future query({required Map query}) async { - const String path = '/graphql'; - - final Map<String, dynamic> params = { - 'query': query, - }; - - final Map<String, String> headers = { - 'x-sdk-graphql': 'true', 'content-type': 'application/json', - }; - - final cacheModel = ''; - final cacheKey = ''; - final cacheResponseIdKey = '\$id'; - final cacheResponseContainerKey = ''; - - final res = await client.call( - HttpMethod.post, - path: path, - params: params, - headers: headers, - cacheModel: cacheModel, - cacheKey: cacheKey, - cacheResponseIdKey: cacheResponseIdKey, - cacheResponseContainerKey: cacheResponseContainerKey, - ); - - return res.data; - - } - - /// GraphQL Endpoint - /// - /// Execute a GraphQL mutation. - /// - Future mutation({required Map query}) async { - const String path = '/graphql/mutation'; - - final Map<String, dynamic> params = { - 'query': query, - }; - - final Map<String, String> headers = { - 'x-sdk-graphql': 'true', 'content-type': 'application/json', - }; - - final cacheModel = ''; - final cacheKey = ''; - final cacheResponseIdKey = '\$id'; - final cacheResponseContainerKey = ''; - - final res = await client.call( - HttpMethod.post, - path: path, - params: params, - headers: headers, - cacheModel: cacheModel, - cacheKey: cacheKey, - cacheResponseIdKey: cacheResponseIdKey, - cacheResponseContainerKey: cacheResponseContainerKey, - ); - - return res.data; - - } -} \ No newline at end of file + Graphql(super.client); + + /// GraphQL Endpoint + /// + /// Execute a GraphQL mutation. + /// + Future query({required Map query}) async { + const String path = '/graphql'; + + final Map<String, dynamic> params = { + 'query': query, + }; + + final Map<String, String> headers = { + 'x-sdk-graphql': 'true', + 'content-type': 'application/json', + }; + + final res = await client.call(CallParams( + HttpMethod.post, + path, + params: params, + headers: headers, + )); + + return res.data; + } + + /// GraphQL Endpoint + /// + /// Execute a GraphQL mutation. + /// + Future mutation({required Map query}) async { + const String path = '/graphql/mutation'; + + final Map<String, dynamic> params = { + 'query': query, + }; + + final Map<String, String> headers = { + 'x-sdk-graphql': 'true', + 'content-type': 'application/json', + }; + + final res = await client.call(CallParams( + HttpMethod.post, + path, + params: params, + headers: headers, + )); + + return res.data; + } +} diff --git a/lib/services/locale.dart b/lib/services/locale.dart index 2bc60905..14fafba6 100644 --- a/lib/services/locale.dart +++ b/lib/services/locale.dart @@ -1,257 +1,180 @@ part of appwrite; - /// The Locale service allows you to customize your app based on your users' - /// location. +/// The Locale service allows you to customize your app based on your users' +/// location. class Locale extends Service { - Locale(super.client); - - /// Get User Locale - /// - /// Get the current user location based on IP. Returns an object with user - /// country code, country name, continent name, continent code, ip address and - /// suggested currency. You can use the locale header to get the data in a - /// supported language. - /// - /// ([IP Geolocation by DB-IP](https://db-ip.com)) - /// - Future<models.Locale> get() async { - const String path = '/locale'; - - final Map<String, dynamic> params = { - }; - - final Map<String, String> headers = { - 'content-type': 'application/json', - }; - - final cacheModel = '/locale'; - final cacheKey = 'current'; - final cacheResponseIdKey = '\$id'; - final cacheResponseContainerKey = ''; - - final res = await client.call( - HttpMethod.get, - path: path, - params: params, - headers: headers, - cacheModel: cacheModel, - cacheKey: cacheKey, - cacheResponseIdKey: cacheResponseIdKey, - cacheResponseContainerKey: cacheResponseContainerKey, - ); - - return models.Locale.fromMap(res.data); - - } - - /// List Continents - /// - /// List of all continents. You can use the locale header to get the data in a - /// supported language. - /// - Future<models.ContinentList> listContinents() async { - const String path = '/locale/continents'; - - final Map<String, dynamic> params = { - }; - - final Map<String, String> headers = { - 'content-type': 'application/json', - }; - - final cacheModel = '/locale/continents'; - final cacheKey = ''; - final cacheResponseIdKey = 'code'; - final cacheResponseContainerKey = 'continents'; - - final res = await client.call( - HttpMethod.get, - path: path, - params: params, - headers: headers, - cacheModel: cacheModel, - cacheKey: cacheKey, - cacheResponseIdKey: cacheResponseIdKey, - cacheResponseContainerKey: cacheResponseContainerKey, - ); - - return models.ContinentList.fromMap(res.data); - - } - - /// List Countries - /// - /// List of all countries. You can use the locale header to get the data in a - /// supported language. - /// - Future<models.CountryList> listCountries() async { - const String path = '/locale/countries'; - - final Map<String, dynamic> params = { - }; - - final Map<String, String> headers = { - 'content-type': 'application/json', - }; - - final cacheModel = '/locale/countries'; - final cacheKey = ''; - final cacheResponseIdKey = 'code'; - final cacheResponseContainerKey = 'countries'; - - final res = await client.call( - HttpMethod.get, - path: path, - params: params, - headers: headers, - cacheModel: cacheModel, - cacheKey: cacheKey, - cacheResponseIdKey: cacheResponseIdKey, - cacheResponseContainerKey: cacheResponseContainerKey, - ); - - return models.CountryList.fromMap(res.data); - - } - - /// List EU Countries - /// - /// List of all countries that are currently members of the EU. You can use the - /// locale header to get the data in a supported language. - /// - Future<models.CountryList> listCountriesEU() async { - const String path = '/locale/countries/eu'; - - final Map<String, dynamic> params = { - }; - - final Map<String, String> headers = { - 'content-type': 'application/json', - }; - - final cacheModel = '/locale/countries/eu'; - final cacheKey = ''; - final cacheResponseIdKey = 'code'; - final cacheResponseContainerKey = 'countries'; - - final res = await client.call( - HttpMethod.get, - path: path, - params: params, - headers: headers, - cacheModel: cacheModel, - cacheKey: cacheKey, - cacheResponseIdKey: cacheResponseIdKey, - cacheResponseContainerKey: cacheResponseContainerKey, - ); - - return models.CountryList.fromMap(res.data); - - } - - /// List Countries Phone Codes - /// - /// List of all countries phone codes. You can use the locale header to get the - /// data in a supported language. - /// - Future<models.PhoneList> listCountriesPhones() async { - const String path = '/locale/countries/phones'; - - final Map<String, dynamic> params = { - }; - - final Map<String, String> headers = { - 'content-type': 'application/json', - }; - - final cacheModel = '/locale/countries/phones'; - final cacheKey = ''; - final cacheResponseIdKey = 'countryCode'; - final cacheResponseContainerKey = 'phones'; - - final res = await client.call( - HttpMethod.get, - path: path, - params: params, - headers: headers, - cacheModel: cacheModel, - cacheKey: cacheKey, - cacheResponseIdKey: cacheResponseIdKey, - cacheResponseContainerKey: cacheResponseContainerKey, - ); - - return models.PhoneList.fromMap(res.data); - - } - - /// List Currencies - /// - /// List of all currencies, including currency symbol, name, plural, and - /// decimal digits for all major and minor currencies. You can use the locale - /// header to get the data in a supported language. - /// - Future<models.CurrencyList> listCurrencies() async { - const String path = '/locale/currencies'; - - final Map<String, dynamic> params = { - }; - - final Map<String, String> headers = { - 'content-type': 'application/json', - }; - - final cacheModel = '/locale/currencies'; - final cacheKey = ''; - final cacheResponseIdKey = 'code'; - final cacheResponseContainerKey = 'currencies'; - - final res = await client.call( - HttpMethod.get, - path: path, - params: params, - headers: headers, - cacheModel: cacheModel, - cacheKey: cacheKey, - cacheResponseIdKey: cacheResponseIdKey, - cacheResponseContainerKey: cacheResponseContainerKey, - ); - - return models.CurrencyList.fromMap(res.data); - - } - - /// List Languages - /// - /// List of all languages classified by ISO 639-1 including 2-letter code, name - /// in English, and name in the respective language. - /// - Future<models.LanguageList> listLanguages() async { - const String path = '/locale/languages'; - - final Map<String, dynamic> params = { - }; - - final Map<String, String> headers = { - 'content-type': 'application/json', - }; - - final cacheModel = '/locale/languages'; - final cacheKey = ''; - final cacheResponseIdKey = 'code'; - final cacheResponseContainerKey = 'languages'; - - final res = await client.call( - HttpMethod.get, - path: path, - params: params, - headers: headers, - cacheModel: cacheModel, - cacheKey: cacheKey, - cacheResponseIdKey: cacheResponseIdKey, - cacheResponseContainerKey: cacheResponseContainerKey, - ); - - return models.LanguageList.fromMap(res.data); - - } -} \ No newline at end of file + Locale(super.client); + + /// Get User Locale + /// + /// Get the current user location based on IP. Returns an object with user + /// country code, country name, continent name, continent code, ip address and + /// suggested currency. You can use the locale header to get the data in a + /// supported language. + /// + /// ([IP Geolocation by DB-IP](https://db-ip.com)) + /// + Future<models.Locale> get() async { + const String path = '/locale'; + + final Map<String, dynamic> params = {}; + + final Map<String, String> headers = { + 'content-type': 'application/json', + }; + + final res = await client.call(CallParams( + HttpMethod.get, + path, + params: params, + headers: headers, + )); + + return models.Locale.fromMap(res.data); + } + + /// List Continents + /// + /// List of all continents. You can use the locale header to get the data in a + /// supported language. + /// + Future<models.ContinentList> listContinents() async { + const String path = '/locale/continents'; + + final Map<String, dynamic> params = {}; + + final Map<String, String> headers = { + 'content-type': 'application/json', + }; + + final res = await client.call(CallParams( + HttpMethod.get, + path, + params: params, + headers: headers, + )); + + return models.ContinentList.fromMap(res.data); + } + + /// List Countries + /// + /// List of all countries. You can use the locale header to get the data in a + /// supported language. + /// + Future<models.CountryList> listCountries() async { + const String path = '/locale/countries'; + + final Map<String, dynamic> params = {}; + + final Map<String, String> headers = { + 'content-type': 'application/json', + }; + + final res = await client.call(CallParams( + HttpMethod.get, + path, + params: params, + headers: headers, + )); + + return models.CountryList.fromMap(res.data); + } + + /// List EU Countries + /// + /// List of all countries that are currently members of the EU. You can use the + /// locale header to get the data in a supported language. + /// + Future<models.CountryList> listCountriesEU() async { + const String path = '/locale/countries/eu'; + + final Map<String, dynamic> params = {}; + + final Map<String, String> headers = { + 'content-type': 'application/json', + }; + + final res = await client.call(CallParams( + HttpMethod.get, + path, + params: params, + headers: headers, + )); + + return models.CountryList.fromMap(res.data); + } + + /// List Countries Phone Codes + /// + /// List of all countries phone codes. You can use the locale header to get the + /// data in a supported language. + /// + Future<models.PhoneList> listCountriesPhones() async { + const String path = '/locale/countries/phones'; + + final Map<String, dynamic> params = {}; + + final Map<String, String> headers = { + 'content-type': 'application/json', + }; + + final res = await client.call(CallParams( + HttpMethod.get, + path, + params: params, + headers: headers, + )); + + return models.PhoneList.fromMap(res.data); + } + + /// List Currencies + /// + /// List of all currencies, including currency symbol, name, plural, and + /// decimal digits for all major and minor currencies. You can use the locale + /// header to get the data in a supported language. + /// + Future<models.CurrencyList> listCurrencies() async { + const String path = '/locale/currencies'; + + final Map<String, dynamic> params = {}; + + final Map<String, String> headers = { + 'content-type': 'application/json', + }; + + final res = await client.call(CallParams( + HttpMethod.get, + path, + params: params, + headers: headers, + )); + + return models.CurrencyList.fromMap(res.data); + } + + /// List Languages + /// + /// List of all languages classified by ISO 639-1 including 2-letter code, name + /// in English, and name in the respective language. + /// + Future<models.LanguageList> listLanguages() async { + const String path = '/locale/languages'; + + final Map<String, dynamic> params = {}; + + final Map<String, String> headers = { + 'content-type': 'application/json', + }; + + final res = await client.call(CallParams( + HttpMethod.get, + path, + params: params, + headers: headers, + )); + + return models.LanguageList.fromMap(res.data); + } +} diff --git a/lib/services/storage.dart b/lib/services/storage.dart index d91754dd..2cd86836 100644 --- a/lib/services/storage.dart +++ b/lib/services/storage.dart @@ -1,271 +1,274 @@ part of appwrite; - /// The Storage service allows you to manage your project files. +/// The Storage service allows you to manage your project files. class Storage extends Service { - Storage(super.client); - - /// List Files - /// - /// Get a list of all the user files. You can use the query params to filter - /// your results. - /// - Future<models.FileList> listFiles({required String bucketId, List<String>? queries, String? search}) async { - final String path = '/storage/buckets/{bucketId}/files'.replaceAll('{bucketId}', bucketId); - - final Map<String, dynamic> params = { - 'queries': queries, - 'search': search, - }; - - final Map<String, String> headers = { - 'content-type': 'application/json', - }; - - final cacheModel = ''; - final cacheKey = ''; - final cacheResponseIdKey = '\$id'; - final cacheResponseContainerKey = 'files'; - - final res = await client.call( - HttpMethod.get, - path: path, - params: params, - headers: headers, - cacheModel: cacheModel, - cacheKey: cacheKey, - cacheResponseIdKey: cacheResponseIdKey, - cacheResponseContainerKey: cacheResponseContainerKey, - ); - - return models.FileList.fromMap(res.data); - - } - - /// Create File - /// - /// Create a new file. Before using this route, you should create a new bucket - /// resource using either a [server - /// integration](/docs/server/storage#storageCreateBucket) API or directly from - /// your Appwrite console. - /// - /// Larger files should be uploaded using multiple requests with the - /// [content-range](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Range) - /// header to send a partial request with a maximum supported chunk of `5MB`. - /// The `content-range` header values should always be in bytes. - /// - /// When the first request is sent, the server will return the **File** object, - /// and the subsequent part request must include the file's **id** in - /// `x-appwrite-id` header to allow the server to know that the partial upload - /// is for the existing file and not for a new one. - /// - /// If you're creating a new file using one of the Appwrite SDKs, all the - /// chunking logic will be managed by the SDK internally. - /// - /// - Future<models.File> createFile({required String bucketId, required String fileId, required InputFile file, List<String>? permissions, Function(UploadProgress)? onProgress}) async { - final String path = '/storage/buckets/{bucketId}/files'.replaceAll('{bucketId}', bucketId); - - final Map<String, dynamic> params = { - - - 'fileId': fileId, - 'file': file, - 'permissions': permissions, - }; - - final Map<String, String> headers = { - 'content-type': 'multipart/form-data', - }; - - String idParamName = ''; - idParamName = 'fileId'; - final paramName = 'file'; - final res = await client.chunkedUpload( - path: path, - params: params, - paramName: paramName, - idParamName: idParamName, - headers: headers, - onProgress: onProgress, - ); - - return models.File.fromMap(res.data); - - } - - /// Get File - /// - /// Get a file by its unique ID. This endpoint response returns a JSON object - /// with the file metadata. - /// - Future<models.File> getFile({required String bucketId, required String fileId}) async { - final String path = '/storage/buckets/{bucketId}/files/{fileId}'.replaceAll('{bucketId}', bucketId).replaceAll('{fileId}', fileId); - - final Map<String, dynamic> params = { - }; - - final Map<String, String> headers = { - 'content-type': 'application/json', - }; - - final cacheModel = ''; - final cacheKey = ''; - final cacheResponseIdKey = '\$id'; - final cacheResponseContainerKey = ''; - - final res = await client.call( - HttpMethod.get, - path: path, - params: params, - headers: headers, - cacheModel: cacheModel, - cacheKey: cacheKey, - cacheResponseIdKey: cacheResponseIdKey, - cacheResponseContainerKey: cacheResponseContainerKey, - ); - - return models.File.fromMap(res.data); - - } - - /// Update File - /// - /// Update a file by its unique ID. Only users with write permissions have - /// access to update this resource. - /// - Future<models.File> updateFile({required String bucketId, required String fileId, List<String>? permissions}) async { - final String path = '/storage/buckets/{bucketId}/files/{fileId}'.replaceAll('{bucketId}', bucketId).replaceAll('{fileId}', fileId); - - final Map<String, dynamic> params = { - 'permissions': permissions, - }; - - final Map<String, String> headers = { - 'content-type': 'application/json', - }; - - final cacheModel = ''; - final cacheKey = ''; - final cacheResponseIdKey = '\$id'; - final cacheResponseContainerKey = ''; - - final res = await client.call( - HttpMethod.put, - path: path, - params: params, - headers: headers, - cacheModel: cacheModel, - cacheKey: cacheKey, - cacheResponseIdKey: cacheResponseIdKey, - cacheResponseContainerKey: cacheResponseContainerKey, - ); - - return models.File.fromMap(res.data); - - } - - /// Delete File - /// - /// Delete a file by its unique ID. Only users with write permissions have - /// access to delete this resource. - /// - Future deleteFile({required String bucketId, required String fileId}) async { - final String path = '/storage/buckets/{bucketId}/files/{fileId}'.replaceAll('{bucketId}', bucketId).replaceAll('{fileId}', fileId); - - final Map<String, dynamic> params = { - }; - - final Map<String, String> headers = { - 'content-type': 'application/json', - }; - - final cacheModel = ''; - final cacheKey = ''; - final cacheResponseIdKey = '\$id'; - final cacheResponseContainerKey = ''; - - final res = await client.call( - HttpMethod.delete, - path: path, - params: params, - headers: headers, - cacheModel: cacheModel, - cacheKey: cacheKey, - cacheResponseIdKey: cacheResponseIdKey, - cacheResponseContainerKey: cacheResponseContainerKey, - ); - - return res.data; - - } - - /// Get File for Download - /// - /// Get a file content by its unique ID. The endpoint response return with a - /// 'Content-Disposition: attachment' header that tells the browser to start - /// downloading the file to user downloads directory. - /// - Future<Uint8List> getFileDownload({required String bucketId, required String fileId}) async { - final String path = '/storage/buckets/{bucketId}/files/{fileId}/download'.replaceAll('{bucketId}', bucketId).replaceAll('{fileId}', fileId); - - final Map<String, dynamic> params = { - - - 'project': client.config['project'], - }; - - final res = await client.call(HttpMethod.get, path: path, params: params, responseType: ResponseType.bytes); - return res.data; - } - - /// Get File Preview - /// - /// Get a file preview image. Currently, this method supports preview for image - /// files (jpg, png, and gif), other supported formats, like pdf, docs, slides, - /// and spreadsheets, will return the file icon image. You can also pass query - /// string arguments for cutting and resizing your preview image. Preview is - /// supported only for image files smaller than 10MB. - /// - Future<Uint8List> getFilePreview({required String bucketId, required String fileId, int? width, int? height, String? gravity, int? quality, int? borderWidth, String? borderColor, int? borderRadius, double? opacity, int? rotation, String? background, String? output}) async { - final String path = '/storage/buckets/{bucketId}/files/{fileId}/preview'.replaceAll('{bucketId}', bucketId).replaceAll('{fileId}', fileId); - - final Map<String, dynamic> params = { - - 'width': width, - 'height': height, - 'gravity': gravity, - 'quality': quality, - 'borderWidth': borderWidth, - 'borderColor': borderColor, - 'borderRadius': borderRadius, - 'opacity': opacity, - 'rotation': rotation, - 'background': background, - 'output': output, - - 'project': client.config['project'], - }; - - final res = await client.call(HttpMethod.get, path: path, params: params, responseType: ResponseType.bytes); - return res.data; - } - - /// Get File for View - /// - /// Get a file content by its unique ID. This endpoint is similar to the - /// download method but returns with no 'Content-Disposition: attachment' - /// header. - /// - Future<Uint8List> getFileView({required String bucketId, required String fileId}) async { - final String path = '/storage/buckets/{bucketId}/files/{fileId}/view'.replaceAll('{bucketId}', bucketId).replaceAll('{fileId}', fileId); - - final Map<String, dynamic> params = { - - - 'project': client.config['project'], - }; - - final res = await client.call(HttpMethod.get, path: path, params: params, responseType: ResponseType.bytes); - return res.data; - } -} \ No newline at end of file + Storage(super.client); + + /// List Files + /// + /// Get a list of all the user files. You can use the query params to filter + /// your results. + /// + Future<models.FileList> listFiles( + {required String bucketId, List<String>? queries, String? search}) async { + final String path = + '/storage/buckets/{bucketId}/files'.replaceAll('{bucketId}', bucketId); + + final Map<String, dynamic> params = { + 'queries': queries, + 'search': search, + }; + + final Map<String, String> headers = { + 'content-type': 'application/json', + }; + + final res = await client.call(CallParams( + HttpMethod.get, + path, + params: params, + headers: headers, + )); + + return models.FileList.fromMap(res.data); + } + + /// Create File + /// + /// Create a new file. Before using this route, you should create a new bucket + /// resource using either a [server + /// integration](/docs/server/storage#storageCreateBucket) API or directly from + /// your Appwrite console. + /// + /// Larger files should be uploaded using multiple requests with the + /// [content-range](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Range) + /// header to send a partial request with a maximum supported chunk of `5MB`. + /// The `content-range` header values should always be in bytes. + /// + /// When the first request is sent, the server will return the **File** object, + /// and the subsequent part request must include the file's **id** in + /// `x-appwrite-id` header to allow the server to know that the partial upload + /// is for the existing file and not for a new one. + /// + /// If you're creating a new file using one of the Appwrite SDKs, all the + /// chunking logic will be managed by the SDK internally. + /// + /// + Future<models.File> createFile( + {required String bucketId, + required String fileId, + required InputFile file, + List<String>? permissions, + Function(UploadProgress)? onProgress}) async { + final String path = + '/storage/buckets/{bucketId}/files'.replaceAll('{bucketId}', bucketId); + + final Map<String, dynamic> params = { + 'fileId': fileId, + 'file': file, + 'permissions': permissions, + }; + + final Map<String, String> headers = { + 'content-type': 'multipart/form-data', + }; + + String idParamName = ''; + idParamName = 'fileId'; + final paramName = 'file'; + final res = await client.chunkedUpload( + path: path, + params: params, + paramName: paramName, + idParamName: idParamName, + headers: headers, + onProgress: onProgress, + ); + + return models.File.fromMap(res.data); + } + + /// Get File + /// + /// Get a file by its unique ID. This endpoint response returns a JSON object + /// with the file metadata. + /// + Future<models.File> getFile( + {required String bucketId, required String fileId}) async { + final String path = '/storage/buckets/{bucketId}/files/{fileId}' + .replaceAll('{bucketId}', bucketId) + .replaceAll('{fileId}', fileId); + + final Map<String, dynamic> params = {}; + + final Map<String, String> headers = { + 'content-type': 'application/json', + }; + + final res = await client.call(CallParams( + HttpMethod.get, + path, + params: params, + headers: headers, + )); + + return models.File.fromMap(res.data); + } + + /// Update File + /// + /// Update a file by its unique ID. Only users with write permissions have + /// access to update this resource. + /// + Future<models.File> updateFile( + {required String bucketId, + required String fileId, + List<String>? permissions}) async { + final String path = '/storage/buckets/{bucketId}/files/{fileId}' + .replaceAll('{bucketId}', bucketId) + .replaceAll('{fileId}', fileId); + + final Map<String, dynamic> params = { + 'permissions': permissions, + }; + + final Map<String, String> headers = { + 'content-type': 'application/json', + }; + + final res = await client.call(CallParams( + HttpMethod.put, + path, + params: params, + headers: headers, + )); + + return models.File.fromMap(res.data); + } + + /// Delete File + /// + /// Delete a file by its unique ID. Only users with write permissions have + /// access to delete this resource. + /// + Future deleteFile({required String bucketId, required String fileId}) async { + final String path = '/storage/buckets/{bucketId}/files/{fileId}' + .replaceAll('{bucketId}', bucketId) + .replaceAll('{fileId}', fileId); + + final Map<String, dynamic> params = {}; + + final Map<String, String> headers = { + 'content-type': 'application/json', + }; + + final res = await client.call(CallParams( + HttpMethod.delete, + path, + params: params, + headers: headers, + )); + + return res.data; + } + + /// Get File for Download + /// + /// Get a file content by its unique ID. The endpoint response return with a + /// 'Content-Disposition: attachment' header that tells the browser to start + /// downloading the file to user downloads directory. + /// + Future<Uint8List> getFileDownload( + {required String bucketId, required String fileId}) async { + final String path = '/storage/buckets/{bucketId}/files/{fileId}/download' + .replaceAll('{bucketId}', bucketId) + .replaceAll('{fileId}', fileId); + + final Map<String, dynamic> params = { + 'project': client.config['project'], + }; + + final res = await client.call(CallParams( + HttpMethod.get, + path, + params: params, + responseType: ResponseType.bytes, + )); + return res.data; + } + + /// Get File Preview + /// + /// Get a file preview image. Currently, this method supports preview for image + /// files (jpg, png, and gif), other supported formats, like pdf, docs, slides, + /// and spreadsheets, will return the file icon image. You can also pass query + /// string arguments for cutting and resizing your preview image. Preview is + /// supported only for image files smaller than 10MB. + /// + Future<Uint8List> getFilePreview( + {required String bucketId, + required String fileId, + int? width, + int? height, + String? gravity, + int? quality, + int? borderWidth, + String? borderColor, + int? borderRadius, + double? opacity, + int? rotation, + String? background, + String? output}) async { + final String path = '/storage/buckets/{bucketId}/files/{fileId}/preview' + .replaceAll('{bucketId}', bucketId) + .replaceAll('{fileId}', fileId); + + final Map<String, dynamic> params = { + 'width': width, + 'height': height, + 'gravity': gravity, + 'quality': quality, + 'borderWidth': borderWidth, + 'borderColor': borderColor, + 'borderRadius': borderRadius, + 'opacity': opacity, + 'rotation': rotation, + 'background': background, + 'output': output, + 'project': client.config['project'], + }; + + final res = await client.call(CallParams( + HttpMethod.get, + path, + params: params, + responseType: ResponseType.bytes, + )); + return res.data; + } + + /// Get File for View + /// + /// Get a file content by its unique ID. This endpoint is similar to the + /// download method but returns with no 'Content-Disposition: attachment' + /// header. + /// + Future<Uint8List> getFileView( + {required String bucketId, required String fileId}) async { + final String path = '/storage/buckets/{bucketId}/files/{fileId}/view' + .replaceAll('{bucketId}', bucketId) + .replaceAll('{fileId}', fileId); + + final Map<String, dynamic> params = { + 'project': client.config['project'], + }; + + final res = await client.call(CallParams( + HttpMethod.get, + path, + params: params, + responseType: ResponseType.bytes, + )); + return res.data; + } +} diff --git a/lib/services/teams.dart b/lib/services/teams.dart index 9532380a..f2762149 100644 --- a/lib/services/teams.dart +++ b/lib/services/teams.dart @@ -1,427 +1,342 @@ part of appwrite; - /// The Teams service allows you to group users of your project and to enable - /// them to share read and write access to your project resources +/// The Teams service allows you to group users of your project and to enable +/// them to share read and write access to your project resources class Teams extends Service { - Teams(super.client); - - /// List Teams - /// - /// Get a list of all the teams in which the current user is a member. You can - /// use the parameters to filter your results. - /// - Future<models.TeamList> list({List<String>? queries, String? search}) async { - const String path = '/teams'; - - final Map<String, dynamic> params = { - 'queries': queries, - 'search': search, - }; - - final Map<String, String> headers = { - 'content-type': 'application/json', - }; - - final cacheModel = '/teams'; - final cacheKey = ''; - final cacheResponseIdKey = '\$id'; - final cacheResponseContainerKey = 'teams'; - - final res = await client.call( - HttpMethod.get, - path: path, - params: params, - headers: headers, - cacheModel: cacheModel, - cacheKey: cacheKey, - cacheResponseIdKey: cacheResponseIdKey, - cacheResponseContainerKey: cacheResponseContainerKey, - ); - - return models.TeamList.fromMap(res.data); - - } - - /// Create Team - /// - /// Create a new team. The user who creates the team will automatically be - /// assigned as the owner of the team. Only the users with the owner role can - /// invite new members, add new owners and delete or update the team. - /// - Future<models.Team> create({required String teamId, required String name, List<String>? roles}) async { - const String path = '/teams'; - - final Map<String, dynamic> params = { - 'teamId': teamId, - 'name': name, - 'roles': roles, - }; - - final Map<String, String> headers = { - 'content-type': 'application/json', - }; - - final cacheModel = ''; - final cacheKey = ''; - final cacheResponseIdKey = '\$id'; - final cacheResponseContainerKey = ''; - - final res = await client.call( - HttpMethod.post, - path: path, - params: params, - headers: headers, - cacheModel: cacheModel, - cacheKey: cacheKey, - cacheResponseIdKey: cacheResponseIdKey, - cacheResponseContainerKey: cacheResponseContainerKey, - ); - - return models.Team.fromMap(res.data); - - } - - /// Get Team - /// - /// Get a team by its ID. All team members have read access for this resource. - /// - Future<models.Team> get({required String teamId}) async { - final String path = '/teams/{teamId}'.replaceAll('{teamId}', teamId); - - final Map<String, dynamic> params = { - }; - - final Map<String, String> headers = { - 'content-type': 'application/json', - }; - - final cacheModel = '/teams'.replaceAll('{teamId}', teamId); - final cacheKey = teamId; - final cacheResponseIdKey = '\$id'; - final cacheResponseContainerKey = ''; - - final res = await client.call( - HttpMethod.get, - path: path, - params: params, - headers: headers, - cacheModel: cacheModel, - cacheKey: cacheKey, - cacheResponseIdKey: cacheResponseIdKey, - cacheResponseContainerKey: cacheResponseContainerKey, - ); - - return models.Team.fromMap(res.data); - - } - - /// Update Team - /// - /// Update a team using its ID. Only members with the owner role can update the - /// team. - /// - Future<models.Team> update({required String teamId, required String name}) async { - final String path = '/teams/{teamId}'.replaceAll('{teamId}', teamId); - - final Map<String, dynamic> params = { - 'name': name, - }; - - final Map<String, String> headers = { - 'content-type': 'application/json', - }; - - final cacheModel = '/teams'.replaceAll('{teamId}', teamId); - final cacheKey = teamId; - final cacheResponseIdKey = '\$id'; - final cacheResponseContainerKey = ''; - - final res = await client.call( - HttpMethod.put, - path: path, - params: params, - headers: headers, - cacheModel: cacheModel, - cacheKey: cacheKey, - cacheResponseIdKey: cacheResponseIdKey, - cacheResponseContainerKey: cacheResponseContainerKey, - ); - - return models.Team.fromMap(res.data); - - } - - /// Delete Team - /// - /// Delete a team using its ID. Only team members with the owner role can - /// delete the team. - /// - Future delete({required String teamId}) async { - final String path = '/teams/{teamId}'.replaceAll('{teamId}', teamId); - - final Map<String, dynamic> params = { - }; - - final Map<String, String> headers = { - 'content-type': 'application/json', - }; - - final cacheModel = ''; - final cacheKey = ''; - final cacheResponseIdKey = '\$id'; - final cacheResponseContainerKey = ''; - - final res = await client.call( - HttpMethod.delete, - path: path, - params: params, - headers: headers, - cacheModel: cacheModel, - cacheKey: cacheKey, - cacheResponseIdKey: cacheResponseIdKey, - cacheResponseContainerKey: cacheResponseContainerKey, - ); - - return res.data; - - } - - /// List Team Memberships - /// - /// Use this endpoint to list a team's members using the team's ID. All team - /// members have read access to this endpoint. - /// - Future<models.MembershipList> listMemberships({required String teamId, List<String>? queries, String? search}) async { - final String path = '/teams/{teamId}/memberships'.replaceAll('{teamId}', teamId); - - final Map<String, dynamic> params = { - 'queries': queries, - 'search': search, - }; - - final Map<String, String> headers = { - 'content-type': 'application/json', - }; - - final cacheModel = '/teams/{teamId}/memberships'.replaceAll('{teamId}', teamId); - final cacheKey = ''; - final cacheResponseIdKey = '\$id'; - final cacheResponseContainerKey = 'memberships'; - - final res = await client.call( - HttpMethod.get, - path: path, - params: params, - headers: headers, - cacheModel: cacheModel, - cacheKey: cacheKey, - cacheResponseIdKey: cacheResponseIdKey, - cacheResponseContainerKey: cacheResponseContainerKey, - ); - - return models.MembershipList.fromMap(res.data); - - } - - /// Create Team Membership - /// - /// Invite a new member to join your team. If initiated from the client SDK, an - /// email with a link to join the team will be sent to the member's email - /// address and an account will be created for them should they not be signed - /// up already. If initiated from server-side SDKs, the new member will - /// automatically be added to the team. - /// - /// Use the 'url' parameter to redirect the user from the invitation email back - /// to your app. When the user is redirected, use the [Update Team Membership - /// Status](/docs/client/teams#teamsUpdateMembershipStatus) endpoint to allow - /// the user to accept the invitation to the team. - /// - /// Please note that to avoid a [Redirect - /// Attack](https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) - /// the only valid redirect URL's are the once from domains you have set when - /// adding your platforms in the console interface. - /// - Future<models.Membership> createMembership({required String teamId, required String email, required List<String> roles, required String url, String? name}) async { - final String path = '/teams/{teamId}/memberships'.replaceAll('{teamId}', teamId); - - final Map<String, dynamic> params = { - 'email': email, - 'roles': roles, - 'url': url, - 'name': name, - }; - - final Map<String, String> headers = { - 'content-type': 'application/json', - }; - - final cacheModel = ''; - final cacheKey = ''; - final cacheResponseIdKey = '\$id'; - final cacheResponseContainerKey = ''; - - final res = await client.call( - HttpMethod.post, - path: path, - params: params, - headers: headers, - cacheModel: cacheModel, - cacheKey: cacheKey, - cacheResponseIdKey: cacheResponseIdKey, - cacheResponseContainerKey: cacheResponseContainerKey, - ); - - return models.Membership.fromMap(res.data); - - } - - /// Get Team Membership - /// - /// Get a team member by the membership unique id. All team members have read - /// access for this resource. - /// - Future<models.Membership> getMembership({required String teamId, required String membershipId}) async { - final String path = '/teams/{teamId}/memberships/{membershipId}'.replaceAll('{teamId}', teamId).replaceAll('{membershipId}', membershipId); - - final Map<String, dynamic> params = { - }; - - final Map<String, String> headers = { - 'content-type': 'application/json', - }; - - final cacheModel = '/teams/{teamId}/memberships'.replaceAll('{teamId}', teamId).replaceAll('{membershipId}', membershipId); - final cacheKey = membershipId; - final cacheResponseIdKey = '\$id'; - final cacheResponseContainerKey = ''; - - final res = await client.call( - HttpMethod.get, - path: path, - params: params, - headers: headers, - cacheModel: cacheModel, - cacheKey: cacheKey, - cacheResponseIdKey: cacheResponseIdKey, - cacheResponseContainerKey: cacheResponseContainerKey, - ); - - return models.Membership.fromMap(res.data); - - } - - /// Update Membership Roles - /// - /// Modify the roles of a team member. Only team members with the owner role - /// have access to this endpoint. Learn more about [roles and - /// permissions](/docs/permissions). - /// - Future<models.Membership> updateMembershipRoles({required String teamId, required String membershipId, required List<String> roles}) async { - final String path = '/teams/{teamId}/memberships/{membershipId}'.replaceAll('{teamId}', teamId).replaceAll('{membershipId}', membershipId); - - final Map<String, dynamic> params = { - 'roles': roles, - }; - - final Map<String, String> headers = { - 'content-type': 'application/json', - }; - - final cacheModel = ''; - final cacheKey = ''; - final cacheResponseIdKey = '\$id'; - final cacheResponseContainerKey = ''; - - final res = await client.call( - HttpMethod.patch, - path: path, - params: params, - headers: headers, - cacheModel: cacheModel, - cacheKey: cacheKey, - cacheResponseIdKey: cacheResponseIdKey, - cacheResponseContainerKey: cacheResponseContainerKey, - ); - - return models.Membership.fromMap(res.data); - - } - - /// Delete Team Membership - /// - /// This endpoint allows a user to leave a team or for a team owner to delete - /// the membership of any other team member. You can also use this endpoint to - /// delete a user membership even if it is not accepted. - /// - Future deleteMembership({required String teamId, required String membershipId}) async { - final String path = '/teams/{teamId}/memberships/{membershipId}'.replaceAll('{teamId}', teamId).replaceAll('{membershipId}', membershipId); - - final Map<String, dynamic> params = { - }; - - final Map<String, String> headers = { - 'content-type': 'application/json', - }; - - final cacheModel = ''; - final cacheKey = ''; - final cacheResponseIdKey = '\$id'; - final cacheResponseContainerKey = ''; - - final res = await client.call( - HttpMethod.delete, - path: path, - params: params, - headers: headers, - cacheModel: cacheModel, - cacheKey: cacheKey, - cacheResponseIdKey: cacheResponseIdKey, - cacheResponseContainerKey: cacheResponseContainerKey, - ); - - return res.data; - - } - - /// Update Team Membership Status - /// - /// Use this endpoint to allow a user to accept an invitation to join a team - /// after being redirected back to your app from the invitation email received - /// by the user. - /// - /// If the request is successful, a session for the user is automatically - /// created. - /// - /// - Future<models.Membership> updateMembershipStatus({required String teamId, required String membershipId, required String userId, required String secret}) async { - final String path = '/teams/{teamId}/memberships/{membershipId}/status'.replaceAll('{teamId}', teamId).replaceAll('{membershipId}', membershipId); - - final Map<String, dynamic> params = { - 'userId': userId, - 'secret': secret, - }; - - final Map<String, String> headers = { - 'content-type': 'application/json', - }; - - final cacheModel = ''; - final cacheKey = ''; - final cacheResponseIdKey = '\$id'; - final cacheResponseContainerKey = ''; - - final res = await client.call( - HttpMethod.patch, - path: path, - params: params, - headers: headers, - cacheModel: cacheModel, - cacheKey: cacheKey, - cacheResponseIdKey: cacheResponseIdKey, - cacheResponseContainerKey: cacheResponseContainerKey, - ); - - return models.Membership.fromMap(res.data); - - } -} \ No newline at end of file + Teams(super.client); + + /// List Teams + /// + /// Get a list of all the teams in which the current user is a member. You can + /// use the parameters to filter your results. + /// + Future<models.TeamList> list({List<String>? queries, String? search}) async { + const String path = '/teams'; + + final Map<String, dynamic> params = { + 'queries': queries, + 'search': search, + }; + + final Map<String, String> headers = { + 'content-type': 'application/json', + }; + + final res = await client.call(CallParams( + HttpMethod.get, + path, + params: params, + headers: headers, + )); + + return models.TeamList.fromMap(res.data); + } + + /// Create Team + /// + /// Create a new team. The user who creates the team will automatically be + /// assigned as the owner of the team. Only the users with the owner role can + /// invite new members, add new owners and delete or update the team. + /// + Future<models.Team> create( + {required String teamId, + required String name, + List<String>? roles}) async { + const String path = '/teams'; + + final Map<String, dynamic> params = { + 'teamId': teamId, + 'name': name, + 'roles': roles, + }; + + final Map<String, String> headers = { + 'content-type': 'application/json', + }; + + final res = await client.call(CallParams( + HttpMethod.post, + path, + params: params, + headers: headers, + )); + + return models.Team.fromMap(res.data); + } + + /// Get Team + /// + /// Get a team by its ID. All team members have read access for this resource. + /// + Future<models.Team> get({required String teamId}) async { + final String path = '/teams/{teamId}'.replaceAll('{teamId}', teamId); + + final Map<String, dynamic> params = {}; + + final Map<String, String> headers = { + 'content-type': 'application/json', + }; + + final res = await client.call(CallParams( + HttpMethod.get, + path, + params: params, + headers: headers, + )); + + return models.Team.fromMap(res.data); + } + + /// Update Team + /// + /// Update a team using its ID. Only members with the owner role can update the + /// team. + /// + Future<models.Team> update( + {required String teamId, required String name}) async { + final String path = '/teams/{teamId}'.replaceAll('{teamId}', teamId); + + final Map<String, dynamic> params = { + 'name': name, + }; + + final Map<String, String> headers = { + 'content-type': 'application/json', + }; + + final res = await client.call(CallParams( + HttpMethod.put, + path, + params: params, + headers: headers, + )); + + return models.Team.fromMap(res.data); + } + + /// Delete Team + /// + /// Delete a team using its ID. Only team members with the owner role can + /// delete the team. + /// + Future delete({required String teamId}) async { + final String path = '/teams/{teamId}'.replaceAll('{teamId}', teamId); + + final Map<String, dynamic> params = {}; + + final Map<String, String> headers = { + 'content-type': 'application/json', + }; + + final res = await client.call(CallParams( + HttpMethod.delete, + path, + params: params, + headers: headers, + )); + + return res.data; + } + + /// List Team Memberships + /// + /// Use this endpoint to list a team's members using the team's ID. All team + /// members have read access to this endpoint. + /// + Future<models.MembershipList> listMemberships( + {required String teamId, List<String>? queries, String? search}) async { + final String path = + '/teams/{teamId}/memberships'.replaceAll('{teamId}', teamId); + + final Map<String, dynamic> params = { + 'queries': queries, + 'search': search, + }; + + final Map<String, String> headers = { + 'content-type': 'application/json', + }; + + final res = await client.call(CallParams( + HttpMethod.get, + path, + params: params, + headers: headers, + )); + + return models.MembershipList.fromMap(res.data); + } + + /// Create Team Membership + /// + /// Invite a new member to join your team. If initiated from the client SDK, an + /// email with a link to join the team will be sent to the member's email + /// address and an account will be created for them should they not be signed + /// up already. If initiated from server-side SDKs, the new member will + /// automatically be added to the team. + /// + /// Use the 'url' parameter to redirect the user from the invitation email back + /// to your app. When the user is redirected, use the [Update Team Membership + /// Status](/docs/client/teams#teamsUpdateMembershipStatus) endpoint to allow + /// the user to accept the invitation to the team. + /// + /// Please note that to avoid a [Redirect + /// Attack](https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) + /// the only valid redirect URL's are the once from domains you have set when + /// adding your platforms in the console interface. + /// + Future<models.Membership> createMembership( + {required String teamId, + required String email, + required List<String> roles, + required String url, + String? name}) async { + final String path = + '/teams/{teamId}/memberships'.replaceAll('{teamId}', teamId); + + final Map<String, dynamic> params = { + 'email': email, + 'roles': roles, + 'url': url, + 'name': name, + }; + + final Map<String, String> headers = { + 'content-type': 'application/json', + }; + + final res = await client.call(CallParams( + HttpMethod.post, + path, + params: params, + headers: headers, + )); + + return models.Membership.fromMap(res.data); + } + + /// Get Team Membership + /// + /// Get a team member by the membership unique id. All team members have read + /// access for this resource. + /// + Future<models.Membership> getMembership( + {required String teamId, required String membershipId}) async { + final String path = '/teams/{teamId}/memberships/{membershipId}' + .replaceAll('{teamId}', teamId) + .replaceAll('{membershipId}', membershipId); + + final Map<String, dynamic> params = {}; + + final Map<String, String> headers = { + 'content-type': 'application/json', + }; + + final res = await client.call(CallParams( + HttpMethod.get, + path, + params: params, + headers: headers, + )); + + return models.Membership.fromMap(res.data); + } + + /// Update Membership Roles + /// + /// Modify the roles of a team member. Only team members with the owner role + /// have access to this endpoint. Learn more about [roles and + /// permissions](/docs/permissions). + /// + Future<models.Membership> updateMembershipRoles( + {required String teamId, + required String membershipId, + required List<String> roles}) async { + final String path = '/teams/{teamId}/memberships/{membershipId}' + .replaceAll('{teamId}', teamId) + .replaceAll('{membershipId}', membershipId); + + final Map<String, dynamic> params = { + 'roles': roles, + }; + + final Map<String, String> headers = { + 'content-type': 'application/json', + }; + + final res = await client.call(CallParams( + HttpMethod.patch, + path, + params: params, + headers: headers, + )); + + return models.Membership.fromMap(res.data); + } + + /// Delete Team Membership + /// + /// This endpoint allows a user to leave a team or for a team owner to delete + /// the membership of any other team member. You can also use this endpoint to + /// delete a user membership even if it is not accepted. + /// + Future deleteMembership( + {required String teamId, required String membershipId}) async { + final String path = '/teams/{teamId}/memberships/{membershipId}' + .replaceAll('{teamId}', teamId) + .replaceAll('{membershipId}', membershipId); + + final Map<String, dynamic> params = {}; + + final Map<String, String> headers = { + 'content-type': 'application/json', + }; + + final res = await client.call(CallParams( + HttpMethod.delete, + path, + params: params, + headers: headers, + )); + + return res.data; + } + + /// Update Team Membership Status + /// + /// Use this endpoint to allow a user to accept an invitation to join a team + /// after being redirected back to your app from the invitation email received + /// by the user. + /// + /// If the request is successful, a session for the user is automatically + /// created. + /// + /// + Future<models.Membership> updateMembershipStatus( + {required String teamId, + required String membershipId, + required String userId, + required String secret}) async { + final String path = '/teams/{teamId}/memberships/{membershipId}/status' + .replaceAll('{teamId}', teamId) + .replaceAll('{membershipId}', membershipId); + + final Map<String, dynamic> params = { + 'userId': userId, + 'secret': secret, + }; + + final Map<String, String> headers = { + 'content-type': 'application/json', + }; + + final res = await client.call(CallParams( + HttpMethod.patch, + path, + params: params, + headers: headers, + )); + + return models.Membership.fromMap(res.data); + } +} diff --git a/lib/src/call_handlers/call_handler.dart b/lib/src/call_handlers/call_handler.dart new file mode 100644 index 00000000..1e6fb404 --- /dev/null +++ b/lib/src/call_handlers/call_handler.dart @@ -0,0 +1,12 @@ +import '../call_params.dart'; +import '../response.dart'; + +abstract class CallHandler { + late CallHandler next; + + void setNext(CallHandler next) { + this.next = next; + } + + Future<Response> handleCall(CallParams params); +} diff --git a/lib/src/call_handlers/cookie_auth_call_handler.dart b/lib/src/call_handlers/cookie_auth_call_handler.dart new file mode 100644 index 00000000..e2455fef --- /dev/null +++ b/lib/src/call_handlers/cookie_auth_call_handler.dart @@ -0,0 +1,42 @@ +import 'dart:io'; + +import 'package:cookie_jar/cookie_jar.dart'; + +import '../call_params.dart'; +import '../response.dart'; +import 'call_handler.dart'; +import 'http_call_handler.dart'; + +class CookieAuthCallHandler extends CallHandler { + final CookieJar cookieJar; + + CookieAuthCallHandler(this.cookieJar); + + @override + Future<Response> handleCall(CallParams params) async { + final endpoint = getEndpoint(params); + final uri = Uri.parse(endpoint + params.path); + try { + final cookies = await cookieJar.loadForRequest(uri); + var cookie = + cookies.map((cookie) => '${cookie.name}=${cookie.value}').join('; '); + if (cookie.isNotEmpty) { + params.headers.addAll({HttpHeaders.cookieHeader: cookie}); + } + } catch (_) {} + + final response = await next.handleCall(params); + + final cookie = response.headers[HttpHeaders.setCookieHeader]; + if (cookie == null) return response; + + var exp = RegExp(r',(?=[^ ])'); + var cookies = cookie.split(exp); + await cookieJar.saveFromResponse( + Uri(scheme: uri.scheme, host: uri.host), + cookies.map((str) => Cookie.fromSetCookieValue(str)).toList(), + ); + + return response; + } +} diff --git a/lib/src/call_handlers/fallback_auth_call_handler.dart b/lib/src/call_handlers/fallback_auth_call_handler.dart new file mode 100644 index 00000000..837e999c --- /dev/null +++ b/lib/src/call_handlers/fallback_auth_call_handler.dart @@ -0,0 +1,31 @@ +import 'package:flutter/foundation.dart'; +import 'package:universal_html/html.dart' as html; + +import '../call_params.dart'; +import '../response.dart'; +import 'call_handler.dart'; + +const _cookieFallbackKey = 'cookieFallback'; + +class FallbackAuthCallHandler extends CallHandler { + FallbackAuthCallHandler(); + + @override + Future<Response> handleCall(CallParams params) async { + if (html.window.localStorage.keys.contains(_cookieFallbackKey)) { + final cookieFallback = html.window.localStorage[_cookieFallbackKey]; + params.headers.addAll({'x-fallback-cookies': cookieFallback!}); + } + + final response = await next.handleCall(params); + + final cookieFallback = response.headers['x-fallback-cookies']; + if (cookieFallback != null) { + debugPrint( + 'Appwrite is using localStorage for session management. Increase your security by adding a custom domain as your API endpoint.'); + html.window.localStorage[_cookieFallbackKey] = cookieFallback; + } + + return response; + } +} diff --git a/lib/src/call_handlers/http_call_handler.dart b/lib/src/call_handlers/http_call_handler.dart new file mode 100644 index 00000000..b72648a6 --- /dev/null +++ b/lib/src/call_handlers/http_call_handler.dart @@ -0,0 +1,51 @@ +import 'package:http/http.dart' as http; + +import '../call_params.dart'; +import '../client_mixin.dart'; +import '../exception.dart'; +import '../response.dart'; +import 'call_handler.dart'; + +const endpointKey = 'endpoint'; + +String getEndpoint(CallParams params) { + final endpoint = params.context[endpointKey]; + if (endpoint == null) return 'https://HOSTNAME/v1'; + return endpoint as String; +} + +CallParams withEndpoint(CallParams params, String endpoint) { + params.context[endpointKey] = endpoint; + return params; +} + +class HttpCallHandler extends CallHandler with ClientMixin { + final http.Client client; + + HttpCallHandler(this.client); + + @override + Future<Response> handleCall(CallParams params) async { + final endpoint = getEndpoint(params); + + late http.Response res; + http.BaseRequest request = prepareRequest( + params.method, + uri: Uri.parse(endpoint + params.path), + headers: params.headers, + params: params.params, + ); + + try { + final streamedResponse = await client.send(request); + res = await toResponse(streamedResponse); + + return prepareResponse(res, responseType: params.responseType); + } catch (e) { + if (e is AppwriteException) { + rethrow; + } + throw AppwriteException(e.toString()); + } + } +} diff --git a/lib/src/call_handlers/offline_call_handler.dart b/lib/src/call_handlers/offline_call_handler.dart new file mode 100644 index 00000000..b20482d1 --- /dev/null +++ b/lib/src/call_handlers/offline_call_handler.dart @@ -0,0 +1,209 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:http/http.dart' as http; + +import '../call_params.dart'; +import '../client_mixin.dart'; +import '../client_offline_mixin.dart'; +import '../enums.dart'; +import '../exception.dart'; +import '../offline/caller.dart'; +import '../offline/route.dart'; +import '../offline/route_mapping.dart'; +import '../offline/router.dart'; +import '../response.dart'; +import 'call_handler.dart'; +import 'http_call_handler.dart'; + +const offlinePersistencyKey = 'offlinePersistency'; + +bool getOfflinePersistency(CallParams params) { + final offlinePersistency = params.context[offlinePersistencyKey]; + if (offlinePersistency == null) return false; + return offlinePersistency as bool; +} + +CallParams withOfflinePersistency(CallParams params, bool status) { + params.context[offlinePersistencyKey] = status; + return params; +} + +class CacheParams { + final String model; + final String key; + final String responseIdKey; + final String responseContainerKey; + final Map<String, Object?>? previous; + + CacheParams({ + this.model = '', + this.key = '', + this.responseIdKey = '', + this.responseContainerKey = '', + this.previous, + }); +} + +const cacheParamsKey = 'cacheParams'; + +CacheParams getCacheParams(CallParams params) { + final cacheParams = params.context[cacheParamsKey]; + if (cacheParams == null) return CacheParams(); + return cacheParams as CacheParams; +} + +CallParams withCacheParams(CallParams params, CacheParams cacheParams) { + params.context[cacheParamsKey] = cacheParams; + return params; +} + +class OfflineCallHandler extends CallHandler + with ClientMixin, ClientOfflineMixin { + final Caller _call; + final Router _router = Router(); + + OfflineCallHandler(this._call) { + routes.forEach((route) { + final method = (route['method'] as String).toUpperCase(); + final path = route['path'] as String; + final offline = route['offline'] as Map<String, String>; + _router.addRoute( + Route(method, path) + .label('offline.model', offline['model']!) + .label('offline.key', offline['key']!) + .label('offline.response-key', offline['response-key']!) + .label('offline.container-key', offline['container-key']!), + ); + }); + } + + @override + Future<Response> handleCall(CallParams params) async { + final endpoint = getEndpoint(params); + final offlinePersistency = getOfflinePersistency(params); + + if (offlinePersistency) { + params.headers['X-SDK-Offline'] = 'true'; + } + + while (true) { + try { + // if offline, do offline stuff + print('checking offline status...'); + + final routeMatch = _router.match( + params.method.name(), + params.path, + ); + + final modelPattern = routeMatch?.getLabel('offline.model') as String; + + final pathValues = routeMatch?.getPathValues(params.path); + + String replacePlaceholder( + String input, Map<String, String>? pathValues) { + if (!input.startsWith('{') || !input.endsWith('}')) { + return input; + } + return pathValues![input.substring(1, input.length - 1)]!; + } + + final model = modelPattern.split('/').map((part) { + return replacePlaceholder(part, pathValues); + }).join('/'); + + final keyPattern = routeMatch?.getLabel('offline.key') ?? ''; + final key = replacePlaceholder( + keyPattern, + pathValues, + ); + + final containerKeyPattern = + routeMatch?.getLabel('offline.container-key') ?? ''; + final containerKey = replacePlaceholder( + containerKeyPattern, + pathValues, + ); + + final cacheParams = CacheParams( + model: model, + key: key, + responseIdKey: routeMatch?.getLabel('offline.response-key') as String, + responseContainerKey: containerKey, + ); + + final uri = Uri.parse(endpoint + params.path); + + http.BaseRequest request = prepareRequest( + params.method, + uri: Uri.parse(endpoint + params.path), + headers: params.headers, + params: params.params, + ); + + if (offlinePersistency && !isOnline.value) { + await checkOnlineStatus(); + } + + if (cacheParams.model.isNotEmpty && + offlinePersistency && + !isOnline.value && + params.responseType != ResponseType.bytes) { + return handleOfflineRequest( + uri: uri, + method: params.method, + call: this._call, + path: params.path, + headers: params.headers, + params: params.params, + responseType: params.responseType, + cacheModel: cacheParams.model, + cacheKey: cacheParams.key, + cacheResponseIdKey: cacheParams.responseIdKey, + cacheResponseContainerKey: cacheParams.responseContainerKey, + ); + } + + final response = await next.handleCall(params); + + if (offlinePersistency) { + // cache stuff + print('cached stuff...'); + + final relationsHeader = response.headers['x-appwrite-relations']; + if (relationsHeader != null) { + final relations = (jsonDecode(relationsHeader) as List) + .cast<Map<String, dynamic>>(); + await cacheCollections(relations); + } + cacheResponse( + cacheModel: cacheParams.model, + cacheKey: cacheParams.key, + cacheResponseIdKey: cacheParams.responseIdKey, + cacheResponseContainerKey: cacheParams.responseContainerKey, + requestMethod: request.method, + responseData: response.data, + ); + } + + return response; + } on AppwriteException catch (e) { + if ((e.message != "Network is unreachable" && + !(e.message?.contains("Failed host lookup") ?? false)) || + !offlinePersistency) { + rethrow; + } + isOnline.value = false; + } on SocketException catch (_) { + if (!offlinePersistency) { + rethrow; + } + isOnline.value = false; + } catch (e, s) { + print(s); + throw AppwriteException(e.toString()); + } + } + } +} diff --git a/lib/src/call_params.dart b/lib/src/call_params.dart new file mode 100644 index 00000000..baa171d6 --- /dev/null +++ b/lib/src/call_params.dart @@ -0,0 +1,23 @@ +import 'enums.dart'; + +class CallParams { + final HttpMethod method; + final String path; + final Map<String, String> headers = {}; + final Map<String, dynamic> params = {}; + final ResponseType? responseType; + final Map<String, Object?> context = {}; + + CallParams( + this.method, + this.path, { + this.responseType, + Map<String, String> headers = const {}, + Map<String, dynamic> params = const {}, + Map<String, Object?> context = const {}, + }) { + this.headers.addAll(headers); + this.params.addAll(params); + this.context.addAll(context); + } +} diff --git a/lib/src/client.dart b/lib/src/client.dart index 88cb62d7..cf9aa2b1 100644 --- a/lib/src/client.dart +++ b/lib/src/client.dart @@ -1,7 +1,8 @@ +import 'call_handlers/call_handler.dart'; +import 'call_params.dart'; import 'client_stub.dart' if (dart.library.html) 'client_browser.dart' if (dart.library.io) 'client_io.dart'; -import 'enums.dart'; import 'response.dart'; import 'upload_progress.dart'; @@ -39,24 +40,14 @@ abstract class Client { /// Your project ID Client setProject(value); + /// Your secret JSON Web Token Client setJWT(value); Client setLocale(value); Client addHeader(String key, String value); - Future<Response> call( - HttpMethod method, { - String path = '', - Map<String, String> headers = const {}, - Map<String, dynamic> params = const {}, - ResponseType? responseType, - String cacheModel = '', - String cacheKey = '', - String cacheResponseIdKey = '', - String cacheResponseContainerKey = '', - Map<String, Object?>? previous, - }); + Future<Response> call(CallParams params); Future<Client> setOfflinePersistency({ bool status = true, @@ -68,4 +59,6 @@ abstract class Client { Client setOfflineCacheSize(int kbytes); int getOfflineCacheSize(); + + Client addHandler(CallHandler handler); } diff --git a/lib/src/client_base.dart b/lib/src/client_base.dart index cdfe8a82..375f0a96 100644 --- a/lib/src/client_base.dart +++ b/lib/src/client_base.dart @@ -1,57 +1,37 @@ +import 'call_handlers/call_handler.dart'; +import 'call_params.dart'; import 'client.dart'; -import 'enums.dart'; import 'response.dart'; abstract class ClientBase implements Client { /// Your project ID - @override ClientBase setProject(value); /// Your secret JSON Web Token - @override ClientBase setJWT(value); - @override ClientBase setLocale(value); - @override ClientBase setSelfSigned({bool status = true}); - @override ClientBase setEndpoint(String endPoint); - @override Client setEndPointRealtime(String endPoint); - @override ClientBase addHeader(String key, String value); - @override - Future<Response> call( - HttpMethod method, { - String path = '', - Map<String, String> headers = const {}, - Map<String, dynamic> params = const {}, - ResponseType? responseType, - String cacheModel = '', - String cacheKey = '', - String cacheResponseIdKey = '', - String cacheResponseContainerKey = '', - Map<String, Object?>? previous, - }); + Future<Response> call(CallParams params); - @override Future<ClientBase> setOfflinePersistency({ bool status = true, void Function(Object)? onWriteQueueError, }); - @override bool getOfflinePersistency(); - @override ClientBase setOfflineCacheSize(int kbytes); - @override int getOfflineCacheSize(); + + ClientBase addHandler(CallHandler handler); } diff --git a/lib/src/client_browser.dart b/lib/src/client_browser.dart index 79db3dbd..83054ee9 100644 --- a/lib/src/client_browser.dart +++ b/lib/src/client_browser.dart @@ -1,15 +1,16 @@ -import 'dart:io'; import 'dart:math'; -import 'package:flutter/foundation.dart'; import 'package:flutter_web_auth_2/flutter_web_auth_2.dart'; import 'package:http/browser_client.dart'; import 'package:http/http.dart' as http; -import 'package:universal_html/html.dart' as html; +import 'call_handlers/call_handler.dart'; +import 'call_handlers/fallback_auth_call_handler.dart'; +import 'call_handlers/http_call_handler.dart'; +import 'call_handlers/offline_call_handler.dart'; +import 'call_params.dart'; import 'client_base.dart'; import 'client_mixin.dart'; -import 'client_offline_mixin.dart'; import 'enums.dart'; import 'exception.dart'; import 'input_file.dart'; @@ -22,7 +23,7 @@ ClientBase createClient({ }) => ClientBrowser(endPoint: endPoint, selfSigned: selfSigned); -class ClientBrowser extends ClientBase with ClientMixin, ClientOfflineMixin { +class ClientBrowser extends ClientBase with ClientMixin { static const int CHUNK_SIZE = 5 * 1024 * 1024; String _endPoint; Map<String, String>? _headers; @@ -30,6 +31,8 @@ class ClientBrowser extends ClientBase with ClientMixin, ClientOfflineMixin { late Map<String, String> config; late BrowserClient _httpClient; String? _endPointRealtime; + late CallHandler _handler; + late OfflineCallHandler _offlineHandler; bool _offlinePersistency = false; int _maxCacheSize = 40000; // 40MB @@ -57,6 +60,9 @@ class ClientBrowser extends ClientBase with ClientMixin, ClientOfflineMixin { assert(_endPoint.startsWith(RegExp("http://|https://")), "endPoint $_endPoint must start with 'http'"); + _handler = HttpCallHandler(_httpClient); + _offlineHandler = OfflineCallHandler(call); + init(); } @@ -116,7 +122,7 @@ class ClientBrowser extends ClientBase with ClientMixin, ClientOfflineMixin { _offlinePersistency = status; if (_offlinePersistency) { - await initOffline( + await _offlineHandler.initOffline( call: call, onWriteQueueError: onWriteQueueError, getOfflineCacheSize: getOfflineCacheSize, @@ -146,10 +152,8 @@ class ClientBrowser extends ClientBase with ClientMixin, ClientOfflineMixin { } Future init() async { - if (html.window.localStorage.keys.contains('cookieFallback')) { - addHeader('x-fallback-cookies', - html.window.localStorage['cookieFallback'] ?? ''); - } + addHandler(FallbackAuthCallHandler()); + addHandler(_offlineHandler); _httpClient.withCredentials = true; } @@ -176,23 +180,23 @@ class ClientBrowser extends ClientBase with ClientMixin, ClientOfflineMixin { file.bytes!, filename: file.filename, ); - return call( + return call(CallParams( HttpMethod.post, - path: path, + path, params: params, headers: headers, - ); + )); } var offset = 0; if (idParamName.isNotEmpty && params[idParamName] != 'unique()') { //make a request to check if a file already exists try { - res = await call( + res = await call(CallParams( HttpMethod.get, - path: path + '/' + params[idParamName], + path + '/' + params[idParamName], headers: headers, - ); + )); final int chunksUploaded = res.data['chunksUploaded'] as int; offset = min(size, chunksUploaded * CHUNK_SIZE); } on AppwriteException catch (_) {} @@ -209,8 +213,12 @@ class ClientBrowser extends ClientBase with ClientMixin, ClientOfflineMixin { ); headers['content-range'] = 'bytes $offset-${min<int>(((offset + CHUNK_SIZE) - 1), size)}/$size'; - res = await call(HttpMethod.post, - path: path, headers: headers, params: params); + res = await call(CallParams( + HttpMethod.post, + path, + headers: headers, + params: params, + )); offset += CHUNK_SIZE; if (offset < size) { headers['x-appwrite-id'] = res.data['\$id']; @@ -228,132 +236,16 @@ class ClientBrowser extends ClientBase with ClientMixin, ClientOfflineMixin { } @override - Future<Response> call( - HttpMethod method, { - String path = '', - Map<String, String> headers = const {}, - Map<String, dynamic> params = const {}, - ResponseType? responseType, - String cacheModel = '', - String cacheKey = '', - String cacheResponseIdKey = '', - String cacheResponseContainerKey = '', - Map<String, Object?>? previous, - }) async { - while (true) { - final uri = Uri.parse(endPoint + path); - - http.BaseRequest request = prepareRequest( - method, - uri: uri, - headers: {..._headers!, ...headers}, - params: params, - ); - - if (getOfflinePersistency() && !isOnline.value) { - await checkOnlineStatus(); - } - - if (cacheModel.isNotEmpty && - getOfflinePersistency() && - !isOnline.value && - responseType != ResponseType.bytes) { - return handleOfflineRequest( - uri: uri, - method: method, - call: call, - path: path, - headers: headers, - params: params, - responseType: responseType, - cacheModel: cacheModel, - cacheKey: cacheKey, - cacheResponseIdKey: cacheResponseIdKey, - cacheResponseContainerKey: cacheResponseContainerKey, - ); - } - - try { - final response = await send( - method, - path: path, - headers: headers, - params: params, - responseType: responseType, - cacheModel: cacheModel, - cacheKey: cacheKey, - cacheResponseIdKey: cacheResponseIdKey, - cacheResponseContainerKey: cacheResponseContainerKey, - ); - - if (getOfflinePersistency()) { - cacheResponse( - cacheModel: cacheModel, - cacheKey: cacheKey, - cacheResponseIdKey: cacheResponseIdKey, - request: request, - response: response, - ); - } - - return response; - } on AppwriteException catch (e) { - if ((e.message != "Network is unreachable" && - !(e.message?.contains("Failed host lookup") ?? false)) || - !getOfflinePersistency()) { - rethrow; - } - isOnline.value = false; - } on SocketException catch (_) { - if (!getOfflinePersistency()) { - rethrow; - } - isOnline.value = false; - } catch (e) { - throw AppwriteException(e.toString()); - } - } - } - - Future<Response> send( - HttpMethod method, { - String path = '', - Map<String, String> headers = const {}, - Map<String, dynamic> params = const {}, - ResponseType? responseType, - String cacheModel = '', - String cacheKey = '', - String cacheResponseIdKey = '', - String cacheResponseContainerKey = '', - Map<String, Object?>? previous, - }) async { - await init(); - - late http.Response res; - http.BaseRequest request = prepareRequest( - method, - uri: Uri.parse(_endPoint + path), - headers: {..._headers!, ...headers}, - params: params, + Future<Response> call(CallParams params) async { + params.headers.addAll(this._headers!); + final response = await _handler.handleCall( + withOfflinePersistency( + withEndpoint(params, endPoint), + getOfflinePersistency(), + ), ); - try { - final streamedResponse = await _httpClient.send(request); - res = await toResponse(streamedResponse); - - final cookieFallback = res.headers['x-fallback-cookies']; - if (cookieFallback != null) { - debugPrint( - 'Appwrite is using localStorage for session management. Increase your security by adding a custom domain as your API endpoint.'); - addHeader('X-Fallback-Cookies', cookieFallback); - html.window.localStorage['cookieFallback'] = cookieFallback; - } - return prepareResponse(res, responseType: responseType); - } catch (e) { - if (e is AppwriteException) { - rethrow; - } - throw AppwriteException(e.toString()); - } + + return response; } @override @@ -363,4 +255,11 @@ class ClientBrowser extends ClientBase with ClientMixin, ClientOfflineMixin { callbackUrlScheme: "appwrite-callback-" + config['project']!, ); } + + @override + ClientBrowser addHandler(CallHandler handler) { + handler.setNext(_handler); + _handler = handler; + return this; + } } diff --git a/lib/src/client_io.dart b/lib/src/client_io.dart index 3b29c760..e4f089c5 100644 --- a/lib/src/client_io.dart +++ b/lib/src/client_io.dart @@ -11,14 +11,16 @@ import 'package:http/io_client.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:path_provider/path_provider.dart'; +import 'call_handlers/call_handler.dart'; +import 'call_handlers/cookie_auth_call_handler.dart'; +import 'call_handlers/http_call_handler.dart'; +import 'call_handlers/offline_call_handler.dart'; +import 'call_params.dart'; import 'client_base.dart'; import 'client_mixin.dart'; -import 'client_offline_mixin.dart'; -import 'cookie_manager.dart'; import 'enums.dart'; import 'exception.dart'; import 'input_file.dart'; -import 'interceptor.dart'; import 'response.dart'; import 'upload_progress.dart'; @@ -31,7 +33,7 @@ ClientBase createClient({ selfSigned: selfSigned, ); -class ClientIO extends ClientBase with ClientMixin, ClientOfflineMixin { +class ClientIO extends ClientBase with ClientMixin { static const int CHUNK_SIZE = 5 * 1024 * 1024; String _endPoint; Map<String, String>? _headers; @@ -44,7 +46,8 @@ class ClientIO extends ClientBase with ClientMixin, ClientOfflineMixin { late http.Client _httpClient; late HttpClient _nativeClient; late CookieJar _cookieJar; - final List<Interceptor> _interceptors = []; + late CallHandler _handler; + late OfflineCallHandler _offlineHandler; bool get initProgress => _initProgress; bool get initialized => _initialized; @@ -78,6 +81,10 @@ class ClientIO extends ClientBase with ClientMixin, ClientOfflineMixin { assert(_endPoint.startsWith(RegExp("http://|https://")), "endPoint $_endPoint must start with 'http'"); + + _handler = HttpCallHandler(_httpClient); + _offlineHandler = OfflineCallHandler(call); + init(); } @@ -148,7 +155,7 @@ class ClientIO extends ClientBase with ClientMixin, ClientOfflineMixin { _offlinePersistency = status; if (_offlinePersistency) { - await initOffline( + await _offlineHandler.initOffline( call: call, onWriteQueueError: onWriteQueueError, getOfflineCacheSize: getOfflineCacheSize, @@ -182,7 +189,8 @@ class ClientIO extends ClientBase with ClientMixin, ClientOfflineMixin { _initProgress = true; final Directory cookieDir = await _getCookiePath(); _cookieJar = PersistCookieJar(storage: FileStorage(cookieDir.path)); - _interceptors.add(CookieManager(_cookieJar)); + addHandler(CookieAuthCallHandler(_cookieJar)); + addHandler(_offlineHandler); var device = ''; try { @@ -230,35 +238,6 @@ class ClientIO extends ClientBase with ClientMixin, ClientOfflineMixin { _initProgress = false; } - Future<http.BaseRequest> _interceptRequest(http.BaseRequest request) async { - final body = (request is http.Request) ? request.body : ''; - for (final i in _interceptors) { - request = await i.onRequest(request); - } - - if (request is http.Request) { - assert( - body == request.body, - 'Interceptors should not transform the body of the request' - 'Use Request converter instead', - ); - } - return request; - } - - Future<http.Response> _interceptResponse(http.Response response) async { - final body = response.body; - for (final i in _interceptors) { - response = await i.onResponse(response); - } - - assert( - body == response.body, - 'Interceptors should not transform the body of the response', - ); - return response; - } - @override Future<Response> chunkedUpload({ required String path, @@ -300,23 +279,23 @@ class ClientIO extends ClientBase with ClientMixin, ClientOfflineMixin { filename: file.filename, ); } - return call( + return call(CallParams( HttpMethod.post, - path: path, + path, params: params, headers: headers, - ); + )); } var offset = 0; if (idParamName.isNotEmpty && params[idParamName] != 'unique()') { //make a request to check if a file already exists try { - res = await call( + res = await call(CallParams( HttpMethod.get, - path: path + '/' + params[idParamName], + path + '/' + params[idParamName], headers: headers, - ); + )); final int chunksUploaded = res.data['chunksUploaded'] as int; offset = min(size, chunksUploaded * CHUNK_SIZE); } on AppwriteException catch (_) {} @@ -344,12 +323,12 @@ class ClientIO extends ClientBase with ClientMixin, ClientOfflineMixin { ); headers['content-range'] = 'bytes $offset-${min<int>(((offset + CHUNK_SIZE) - 1), size)}/$size'; - res = await call( + res = await call(CallParams( HttpMethod.post, - path: path, + path, headers: headers, params: params, - ); + )); offset += CHUNK_SIZE; if (offset < size) { headers['x-appwrite-id'] = res.data['\$id']; @@ -393,138 +372,27 @@ class ClientIO extends ClientBase with ClientMixin, ClientOfflineMixin { }); } - Future<Response> send( - HttpMethod method, { - String path = '', - Map<String, String> headers = const {}, - Map<String, dynamic> params = const {}, - ResponseType? responseType, - String cacheModel = '', - String cacheKey = '', - String cacheResponseIdKey = '', - String cacheResponseContainerKey = '', - Map<String, Object?>? previous, - }) async { - while (!_initialized && _initProgress) { - await Future.delayed(Duration(milliseconds: 10)); - } - if (!_initialized) { - await init(); + @override + Future<Response> call(CallParams params) async { + while (!_initialized) { + await Future.delayed(Duration(milliseconds: 100)); } - final uri = Uri.parse(_endPoint + path); - http.BaseRequest request = prepareRequest( - method, - uri: uri, - headers: {..._headers!, ...headers}, - params: params, + params.headers.addAll(this._headers!); + final response = await _handler.handleCall( + withOfflinePersistency( + withEndpoint(params, endPoint), + getOfflinePersistency(), + ), ); - try { - request = await _interceptRequest(request); - final streamedResponse = await _httpClient.send(request); - http.Response res = await toResponse(streamedResponse); - res = await _interceptResponse(res); - - final response = prepareResponse( - res, - responseType: responseType, - ); - - return response; - } catch (e) { - if (e is AppwriteException) { - rethrow; - } - throw AppwriteException(e.toString()); - } + return response; } @override - Future<Response> call( - HttpMethod method, { - String path = '', - Map<String, String> headers = const {}, - Map<String, dynamic> params = const {}, - ResponseType? responseType, - String cacheModel = '', - String cacheKey = '', - String cacheResponseIdKey = '', - String cacheResponseContainerKey = '', - Map<String, Object?>? previous, - }) async { - while (true) { - final uri = Uri.parse(endPoint + path); - - http.BaseRequest request = prepareRequest( - method, - uri: uri, - headers: {..._headers!, ...headers}, - params: params, - ); - - if (getOfflinePersistency() && !isOnline.value) { - await checkOnlineStatus(); - } - - if (cacheModel.isNotEmpty && - getOfflinePersistency() && - !isOnline.value && - responseType != ResponseType.bytes) { - return handleOfflineRequest( - uri: uri, - method: method, - call: call, - path: path, - headers: headers, - params: params, - responseType: responseType, - cacheModel: cacheModel, - cacheKey: cacheKey, - cacheResponseIdKey: cacheResponseIdKey, - cacheResponseContainerKey: cacheResponseContainerKey, - ); - } - - try { - final response = await send( - method, - path: path, - headers: headers, - params: params, - responseType: responseType, - cacheModel: cacheModel, - cacheKey: cacheKey, - cacheResponseIdKey: cacheResponseIdKey, - cacheResponseContainerKey: cacheResponseContainerKey, - ); - - if (getOfflinePersistency()) { - cacheResponse( - cacheModel: cacheModel, - cacheKey: cacheKey, - cacheResponseIdKey: cacheResponseIdKey, - request: request, - response: response, - ); - } - - return response; - } on AppwriteException catch (e) { - if ((e.message != "Network is unreachable" && - !(e.message?.contains("Failed host lookup") ?? false)) || - !getOfflinePersistency()) { - rethrow; - } - isOnline.value = false; - } on SocketException catch (_) { - if (!getOfflinePersistency()) { - rethrow; - } - isOnline.value = false; - } catch (e) { - throw AppwriteException(e.toString()); - } - } + ClientBase addHandler(CallHandler handler) { + handler.setNext(_handler); + _handler = handler; + return this; } } diff --git a/lib/src/client_mixin.dart b/lib/src/client_mixin.dart index 82fede58..d9926b29 100644 --- a/lib/src/client_mixin.dart +++ b/lib/src/client_mixin.dart @@ -1,8 +1,10 @@ +import 'dart:convert'; + import 'package:http/http.dart' as http; + +import 'enums.dart'; import 'exception.dart'; import 'response.dart'; -import 'dart:convert'; -import 'enums.dart'; class ClientMixin { http.BaseRequest prepareRequest( @@ -39,7 +41,7 @@ class ClientMixin { } } else if (method == HttpMethod.get) { if (params.isNotEmpty) { - params = params.map((key, value){ + params = params.map((key, value) { if (value is int || value is double) { return MapEntry(key, value.toString()); } @@ -96,21 +98,26 @@ class ClientMixin { data = res.body; } } - return Response(data: data); + return Response(headers: res.headers, data: data); } - Future<http.Response> toResponse(http.StreamedResponse streamedResponse) async { - if(streamedResponse.statusCode == 204) { - return http.Response('', - streamedResponse.statusCode, - headers: streamedResponse.headers.map((k,v) => k.toLowerCase()=='content-type' ? MapEntry(k, 'text/plain') : MapEntry(k,v)), - request: streamedResponse.request, - isRedirect: streamedResponse.isRedirect, - persistentConnection: streamedResponse.persistentConnection, - reasonPhrase: streamedResponse.reasonPhrase, - ); - } else { - return await http.Response.fromStream(streamedResponse); - } + Future<http.Response> toResponse( + http.StreamedResponse streamedResponse) async { + if (streamedResponse.statusCode == 204) { + return http.Response( + '', + streamedResponse.statusCode, + headers: streamedResponse.headers.map((k, v) => + k.toLowerCase() == 'content-type' + ? MapEntry(k, 'text/plain') + : MapEntry(k, v)), + request: streamedResponse.request, + isRedirect: streamedResponse.isRedirect, + persistentConnection: streamedResponse.persistentConnection, + reasonPhrase: streamedResponse.reasonPhrase, + ); + } else { + return await http.Response.fromStream(streamedResponse); + } } } diff --git a/lib/src/client_offline_mixin.dart b/lib/src/client_offline_mixin.dart index b1a1d4eb..7cf56466 100644 --- a/lib/src/client_offline_mixin.dart +++ b/lib/src/client_offline_mixin.dart @@ -5,8 +5,11 @@ import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; import 'package:sembast/utils/value_utils.dart'; +import 'call_handlers/offline_call_handler.dart'; +import 'call_params.dart'; import 'enums.dart'; import 'exception.dart'; +import 'offline/caller.dart'; import 'offline/services/accessed_at.dart'; import 'offline/services/cache_size.dart'; import 'offline/services/model_data.dart'; @@ -25,18 +28,7 @@ class ClientOfflineMixin { late QueuedWrites _queuedWrites; Future<void> initOffline({ - required Future<Response<dynamic>> Function( - HttpMethod, { - String path, - Map<String, String> headers, - Map<String, dynamic> params, - ResponseType? responseType, - String cacheModel, - String cacheKey, - String cacheResponseIdKey, - String cacheResponseContainerKey, - }) - call, + required Caller call, void Function(Object)? onWriteQueueError, required int Function() getOfflineCacheSize, }) async { @@ -64,20 +56,7 @@ class ClientOfflineMixin { _queuedWrites = QueuedWrites(db); } - Future<void> processWriteQueue( - Future<Response<dynamic>> Function( - HttpMethod, { - String path, - Map<String, String> headers, - Map<String, dynamic> params, - ResponseType? responseType, - String cacheModel, - String cacheKey, - String cacheResponseIdKey, - String cacheResponseContainerKey, - }) - call, - {void Function(Object e)? onError}) async { + Future<void> processWriteQueue(Caller call, {Function? onError}) async { if (!isOnline.value) return; final queuedWrites = await _queuedWrites.list(); for (final queuedWrite in queuedWrites) { @@ -85,16 +64,22 @@ class ClientOfflineMixin { final method = HttpMethod.values .where((v) => v.name() == queuedWrite.method) .first; - final res = await call( - method, - path: queuedWrite.path, - headers: queuedWrite.headers, - params: queuedWrite.params, - cacheModel: queuedWrite.cacheModel, - cacheKey: queuedWrite.cacheKey, - cacheResponseContainerKey: queuedWrite.cacheResponseContainerKey, - cacheResponseIdKey: queuedWrite.cacheResponseIdKey, + + final params = withCacheParams( + CallParams( + method, + queuedWrite.path, + headers: queuedWrite.headers, + params: queuedWrite.params, + ), + CacheParams( + model: queuedWrite.cacheModel, + key: queuedWrite.cacheKey, + responseContainerKey: queuedWrite.cacheResponseContainerKey, + responseIdKey: queuedWrite.cacheResponseIdKey, + ), ); + final res = await call(params); if (method == HttpMethod.post) { await _modelData.upsert( @@ -160,16 +145,7 @@ class ClientOfflineMixin { Future<Response> handleOfflineRequest({ required Uri uri, required HttpMethod method, - required Future<Response<dynamic>> Function(HttpMethod, - {String path, - Map<String, String> headers, - Map<String, dynamic> params, - ResponseType? responseType, - String cacheModel, - String cacheKey, - String cacheResponseIdKey, - String cacheResponseContainerKey}) - call, + required Caller call, String path = '', Map<String, String> headers = const {}, Map<String, dynamic> params = const {}, @@ -320,13 +296,13 @@ class ClientOfflineMixin { } try { - final res = await call( + final res = await call(CallParams( method, + path, headers: headers, params: params, - path: path, responseType: responseType, - ); + )); final futures = <Future>[]; if (method == HttpMethod.post) { @@ -390,45 +366,122 @@ class ClientOfflineMixin { return completer.future; } + void cacheRelated({ + required Map<String, Object?> document, + }) { + // iterate over each attribute to see if it's nested data + document.entries.forEach((entry) { + if (entry.value is Map) { + final nestedDocument = entry.value as Map<String, Object?>; + final nestedDatabaseId = nestedDocument['\$databaseId'] as String?; + final nestedCollectionId = nestedDocument['\$collectionId'] as String?; + final nestedDocumentId = nestedDocument['\$id'] as String?; + if (nestedDatabaseId == null || + nestedCollectionId == null || + nestedDocumentId == null) return; + final nestedModel = + "/databases/$nestedDatabaseId/collections/$nestedCollectionId/documents"; + cacheResponse( + cacheModel: nestedModel, + cacheKey: nestedDocumentId, + cacheResponseIdKey: "\$id", + cacheResponseContainerKey: '', + requestMethod: 'GET', + responseData: entry.value, + ); + document[entry.key] = { + '\$id': nestedDocumentId, + '\$databaseId': nestedDatabaseId, + '\$collectionId': nestedCollectionId, + }; + } else if (entry.value is List && + entry.value != null && + (entry.value as List).isNotEmpty) { + final values = (entry.value as List); + if (!(values.first is Map<String, Object?>)) return; + final nestedDocument = values.first; + final nestedDatabaseId = nestedDocument['\$databaseId'] as String?; + final nestedCollectionId = nestedDocument['\$collectionId'] as String?; + if (nestedDatabaseId == null || nestedCollectionId == null) return; + final nestedModel = + "/databases/$nestedDatabaseId/collections/$nestedCollectionId/documents"; + cacheResponse( + cacheModel: nestedModel, + cacheKey: '', + cacheResponseIdKey: "\$id", + cacheResponseContainerKey: 'documents', + requestMethod: 'GET', + responseData: { + 'documents': entry.value, + }, + ); + document[entry.key] = values.map((value) { + final nestedDocumentId = value['\$id'] as String?; + return { + '\$id': nestedDocumentId, + '\$databaseId': nestedDatabaseId, + '\$collectionId': nestedCollectionId, + }; + }).toList(); + } + }); + } + + Future<void> cacheCollections(List<Map<String, dynamic>> relations) { + final futures = <Future>[]; + for (var collection in relations) { + futures.add(_modelData.cacheModel(collection)); + } + return Future.wait(futures); + } + void cacheResponse({ required String cacheModel, required String cacheKey, required String cacheResponseIdKey, - required http.BaseRequest request, - required Response response, + required String cacheResponseContainerKey, + required String requestMethod, + required dynamic responseData, }) { if (cacheModel.isEmpty) return; - switch (request.method) { + switch (requestMethod) { case 'GET': - final clone = cloneMap(response.data); + final clone = cloneMap(responseData); if (cacheKey.isNotEmpty) { + cacheRelated(document: clone); _modelData.upsert(model: cacheModel, data: clone, key: cacheKey); } else { - clone.forEach((key, value) { - if (key == 'total') return; - _modelData.batchUpsert( - model: cacheModel, - dataList: value as List, - idKey: cacheResponseIdKey, - ); + final values = clone[cacheResponseContainerKey] as List; + values.forEach((value) { + cacheRelated(document: value); }); + _modelData.batchUpsert( + model: cacheModel, + dataList: values, + idKey: cacheResponseIdKey, + ); } break; case 'POST': case 'PUT': case 'PATCH': - Map<String, Object?> clone = cloneMap(response.data); + Map<String, Object?> clone = cloneMap(responseData); if (cacheKey.isEmpty) { cacheKey = clone['\$id'] as String; } if (cacheModel.endsWith('/prefs')) { - clone = response.data['prefs']; + clone = responseData['prefs']; } _modelData.upsert(model: cacheModel, data: clone, key: cacheKey); break; case 'DELETE': if (cacheKey.isNotEmpty) { + // _modelData.get(model: cacheModel, key: cacheKey).then((cachedData) { + // if (cachedData == null) { + // return; + // } + // }); _modelData.delete(model: cacheModel, key: cacheKey); } } diff --git a/lib/src/offline/caller.dart b/lib/src/offline/caller.dart new file mode 100644 index 00000000..8eb6a0f9 --- /dev/null +++ b/lib/src/offline/caller.dart @@ -0,0 +1,4 @@ +import '../call_params.dart'; +import '../response.dart'; + +typedef Caller = Future<Response<dynamic>> Function(CallParams params); diff --git a/lib/src/offline/route.dart b/lib/src/offline/route.dart new file mode 100644 index 00000000..ad008a1e --- /dev/null +++ b/lib/src/offline/route.dart @@ -0,0 +1,47 @@ +class Route { + String method = ''; + String path; + final List<String> _aliases = []; + final Map<String, dynamic> labels = {}; + + final Map<String, int> _pathParams = {}; + + Route(this.method, this.path) : super(); + + List<String> get aliases => _aliases; + Map<String, int> get pathParams => _pathParams; + + Route alias(String path) { + if (!_aliases.contains(path)) { + _aliases.add(path); + } + + return this; + } + + void setPathParam(String key, int index) { + _pathParams[key] = index; + } + + Map<String, String> getPathValues(String path) { + var pathValues = <String, String>{}; + var parts = path.split('/').where((part) => part.isNotEmpty); + + for (var entry in pathParams.entries) { + if (entry.value < parts.length) { + pathValues[entry.key] = parts.elementAt(entry.value); + } + } + + return pathValues; + } + + Route label(String key, String value) { + labels[key] = value; + return this; + } + + String? getLabel(String key, {String? defaultValue}) { + return labels[key] ?? defaultValue; + } +} diff --git a/lib/src/offline/route_mapping.dart b/lib/src/offline/route_mapping.dart new file mode 100644 index 00000000..81784f70 --- /dev/null +++ b/lib/src/offline/route_mapping.dart @@ -0,0 +1,80 @@ +final routes = [ + { + "method": "get", + "path": "\/account", + "offline": { + "model": "\/account", + "key": "current", + "response-key": "\$id", + "container-key": "", + }, + }, + { + "method": "post", + "path": "\/account\/sessions\/email", + "offline": { + "model": "", + "key": "", + "response-key": "\$id", + "container-key": "", + }, + }, + { + "method": "get", + "path": "\/databases\/{databaseId}\/collections\/{collectionId}\/documents", + "offline": { + "model": + "\/databases\/{databaseId}\/collections\/{collectionId}\/documents", + "key": "", + "response-key": "\$id", + "container-key": "documents", + }, + }, + { + "method": "post", + "path": "\/databases\/{databaseId}\/collections\/{collectionId}\/documents", + "offline": { + "model": + "\/databases\/{databaseId}\/collections\/{collectionId}\/documents", + "key": "", + "response-key": "\$id", + "container-key": "", + }, + }, + { + "method": "get", + "path": + "\/databases\/{databaseId}\/collections\/{collectionId}\/documents\/{documentId}", + "offline": { + "model": + "\/databases\/{databaseId}\/collections\/{collectionId}\/documents", + "key": "{documentId}", + "response-key": "\$id", + "container-key": "", + }, + }, + { + "method": "patch", + "path": + "\/databases\/{databaseId}\/collections\/{collectionId}\/documents\/{documentId}", + "offline": { + "model": + "\/databases\/{databaseId}\/collections\/{collectionId}\/documents", + "key": "{documentId}", + "response-key": "\$id", + "container-key": "", + }, + }, + { + "method": "delete", + "path": + "\/databases\/{databaseId}\/collections\/{collectionId}\/documents\/{documentId}", + "offline": { + "model": + "\/databases\/{databaseId}\/collections\/{collectionId}\/documents", + "key": "{documentId}", + "response-key": "\$id", + "container-key": "", + }, + } +]; diff --git a/lib/src/offline/router.dart b/lib/src/offline/router.dart new file mode 100644 index 00000000..7720ddff --- /dev/null +++ b/lib/src/offline/router.dart @@ -0,0 +1,126 @@ +import 'dart:collection'; + +import 'route.dart'; + +class Router { + static const String placeholderToken = ':::'; + + Map<String, Map<String, Route>> _routes = { + 'GET': {}, + 'POST': {}, + 'PUT': {}, + 'PATCH': {}, + 'DELETE': {}, + }; + + List<int> _params = []; + + UnmodifiableMapView<String, Map<String, Route>> getRoutes() { + return UnmodifiableMapView(_routes); + } + + void addRoute(Route route) { + List<dynamic> result = preparePath(route.path); + String path = result[0]; + Map<String, int> params = result[1]; + + if (!_routes.containsKey(route.method)) { + throw Exception("Method (${route.method}) not supported."); + } + + if (_routes[route.method]!.containsKey(path)) { + throw Exception("Route for (${route.method}:$path) already registered."); + } + + params.forEach((key, index) { + route.setPathParam(key, index); + }); + + _routes[route.method]![path] = (route); + + for (String alias in route.aliases) { + List<dynamic> aliasResult = preparePath(alias); + String aliasPath = aliasResult[0]; + _routes[route.method]![aliasPath] = route; + } + } + + Route? match(String method, String path) { + if (!_routes.containsKey(method)) { + return null; + } + + List<String> parts = path.split('/').where((p) => p.isNotEmpty).toList(); + int length = parts.length - 1; + List<int> filteredParams = _params.where((i) => i <= length).toList(); + + for (List<int> sample in combinations<int>(filteredParams)) { + sample = sample.where((i) => i <= length).toList(); + String match = parts + .asMap() + .entries + .map( + (entry) => + sample.contains(entry.key) ? placeholderToken : entry.value, + ) + .join('/'); + + if (_routes[method]!.containsKey(match)) { + return _routes[method]![match]!; + } + } + + return null; + } + + Iterable<List<T>> combinations<T>(List<T> set) { + final result = <List<T>>[[]]; + + for (final element in set) { + final newCombinations = <List<T>>[]; + for (final combination in result) { + final ret = [element, ...combination]; + newCombinations.add(ret); + } + result.addAll(newCombinations); + } + + return result; + } + + List<dynamic> preparePath(String path) { + List<String> parts = path.split('/').where((p) => p.isNotEmpty).toList(); + String prepare = ''; + Map<String, int> params = {}; + + for (int key = 0; key < parts.length; key++) { + String part = parts[key]; + if (key != 0) { + prepare += '/'; + } + + if (part.startsWith('{') && part.endsWith('}')) { + prepare += placeholderToken; + params[part.substring(1, part.length - 1)] = key; + if (!_params.contains(key)) { + _params.add(key); + } + } else { + prepare += part; + } + } + + return [prepare, params]; + } + + void reset() { + _params = []; + _routes = { + 'GET': {}, + 'POST': {}, + 'PUT': {}, + 'PATCH': {}, + 'DELETE': {}, + }; + } +} diff --git a/lib/src/offline/services/cache_size.dart b/lib/src/offline/services/cache_size.dart index cf1b0c87..0934179f 100644 --- a/lib/src/offline/services/cache_size.dart +++ b/lib/src/offline/services/cache_size.dart @@ -19,23 +19,33 @@ class CacheSize { return encoded; } - Future<void> applyChange(int change) async { - if (change == 0) return; + Future<int?> applyChange(Transaction txn, int change) async { + if (change == 0) return null; final record = getCacheSizeRecordRef(); - - final currentSize = await record.get(_db) ?? 0; - await record.put(_db, currentSize + change); + final currentSize = await record.get(txn) ?? 0; + return await record.put(txn, currentSize + change); } Future<void> update({ - Map<String, dynamic>? oldData, + required RecordRef<String, Map<String, Object?>> recordRef, + required Transaction txn, Map<String, dynamic>? newData, }) async { + final oldData = await recordRef.get(txn); final oldSize = oldData != null ? encode(oldData).length : 0; final newSize = newData != null ? encode(newData).length : 0; final change = newSize - oldSize; - await applyChange(change); + final cacheSize = await applyChange(txn, change); + + if (change != 0) { + print([ + '${recordRef.key}: oldSize: $oldSize', + 'newSize: $newSize', + 'change: $change', + 'cacheSize: $cacheSize', + ].join(', ')); + } } void onChange(void callback(int? currentSize)) { diff --git a/lib/src/offline/services/model_data.dart b/lib/src/offline/services/model_data.dart index fbda4788..a158278e 100644 --- a/lib/src/offline/services/model_data.dart +++ b/lib/src/offline/services/model_data.dart @@ -1,5 +1,6 @@ import 'package:appwrite/src/offline/services/cache_size.dart'; import 'package:sembast/sembast.dart'; +import 'package:sembast/utils/value_utils.dart'; import '../../../appwrite.dart'; import 'accessed_at.dart'; @@ -8,6 +9,9 @@ class ModelData { final Database _db; final AccessedAt _accessedAt; final CacheSize _cacheSize; + final int maxDepth = 3; + final documentModelRegex = RegExp( + r'^/databases/([a-zA-Z0-9\-]*)/collections/([a-zA-Z0-9\-]*)/documents$'); ModelData(this._db) : _accessedAt = AccessedAt(_db), @@ -17,15 +21,128 @@ class ModelData { return stringMapStoreFactory.store(model); } + Future<void> cacheModel(Map<String, dynamic> collection) { + final store = stringMapStoreFactory.store('collections'); + + return store + .record("${collection['databaseId']}|${collection['collectionId']}") + .put(_db, collection); + } + Future<Map<String, dynamic>?> get({ required String model, required String key, + }) async { + final immutableRecord = await _getRecord(model: model, key: key); + + if (immutableRecord == null) return null; + + final record = cloneMap(immutableRecord); + await _populateRelated(record, 0); + + return record; + } + + Future<Map<String, dynamic>?> _getRecord({ + required String model, + required String key, }) async { final store = getModelStore(model); final recordRef = store.record(key); return recordRef.get(_db); } + bool _isNestedDocument(Map<String, dynamic> record) { + return record.containsKey('\$databaseId') && + record.containsKey('\$collectionId') && + record.containsKey('\$id'); + } + + /// Given a record with $databaseId, $collectionId and $id, populate the rest + /// of the attributes from the cache and then populate the related records. + Future<Map<String, dynamic>?> _populateRecord( + Map<String, dynamic>? record, int depth) async { + if (record == null) return record; + + if (!_isNestedDocument(record)) return record; + + final databaseId = record['\$databaseId'] as String; + final collectionId = record['\$collectionId'] as String; + final documentId = record['\$id'] as String; + + final nestedModel = + "/databases/$databaseId/collections/$collectionId/documents"; + + final cached = await _getRecord( + model: nestedModel, + key: documentId, + ); + + if (cached == null) return record; + + record.addAll(cloneMap(cached)); + + await _populateRelated(record, depth + 1); + + return record; + } + + /// Iterate over every attribute of a record and fetch related records from + /// the cache. + Future<void> _populateRelated(Map<String, dynamic>? record, int depth) { + if (record == null) { + return Future.value(); + } + + // iterate over each attribute and check if it is a relation + final futures = <Future>[]; + for (final attribute in record.entries) { + if (attribute.value is Map<String, Object?>) { + final map = attribute.value as Map<String, Object?>; + if (_isNestedDocument(record)) { + if (depth >= maxDepth) { + record[attribute.key] = null; + } else { + final future = _populateRecord(map, depth).then((populated) { + record[attribute.key] = populated; + }); + + futures.add(future); + } + } + } else if (attribute.value is List) { + final List list = attribute.value as List; + final futureList = <Future<Map<String, dynamic>?>>[]; + if (list.isEmpty) continue; + + if (depth >= maxDepth && + list.first is Map<String, Object?> && + _isNestedDocument(list.first)) { + record[attribute.key] = []; + continue; + } + + for (final map in list) { + if (map is! Map<String, Object?>) { + continue; + } + futureList.add(_populateRecord(map, depth)); + } + + if (futureList.isEmpty) { + continue; + } + + final future = Future.wait(futureList).then((populated) { + record[attribute.key] = populated; + }); + + futures.add(future); + } + } + return Future.wait(futures); + } + Future<Map<String, dynamic>> list({ required String model, required String cacheResponseContainerKey, @@ -50,6 +167,7 @@ class ModelData { final List<Filter> equalFilters = []; value.forEach((v) { equalFilters.add(Filter.equals(q.params[0], v)); + equalFilters.add(Filter.equals("${q.params[0]}.\$id", v)); }); filters.add(Filter.or(equalFilters)); }); @@ -82,6 +200,30 @@ class ModelData { filters.add(Filter.matches(q.params[0], r'${q.params[1]}+')); break; + case 'isNull': + // TODO: Handle this case. + break; + + case 'isNotNull': + // TODO: Handle this case. + break; + + case 'between': + // TODO: Handle this case. + break; + + case 'startsWith': + // TODO: Handle this case. + break; + + case 'endsWith': + // TODO: Handle this case. + break; + + case 'select': + // TODO: Handle this case. + break; + case 'orderAsc': sortOrders.add(SortOrder(q.params[0] as String)); break; @@ -116,19 +258,27 @@ class ModelData { final records = await store.find(_db, finder: finder); final count = await store.count(_db, filter: filter); + final list = records.map((record) { + // convert to Map<String, dynamic> + final map = Map<String, dynamic>(); + record.value.entries.forEach((entry) { + map[entry.key] = entry.value; + }); + return map; + }).toList(); + + final futures = <Future<Map<String, dynamic>?>>[]; + for (final record in list) { + futures.add(_populateRecord(record, 0)); + } + final keys = records.map((record) => record.key).toList(); _accessedAt.update(model: store.name, keys: keys); return { 'total': count, - cacheResponseContainerKey: records.map((record) { - final map = Map<String, dynamic>(); - record.value.entries.forEach((entry) { - map[entry.key] = entry.value; - }); - return map; - }).toList(), + cacheResponseContainerKey: await Future.wait(futures), }; } @@ -137,13 +287,90 @@ class ModelData { required Map<String, dynamic> data, required String key, }) async { - final store = getModelStore(model); + final match = documentModelRegex.firstMatch(model); + if (match?.groupCount == 2) { + // data is a document + + // match starting at 1 since 0 is the full match + final databaseId = match!.group(1)!; + final collectionId = match.group(2)!; + + final collectionStore = getModelStore('collections'); + final recordRef = collectionStore.record('$databaseId|$collectionId'); + final collection = await recordRef.get(_db); + final attributes = (collection?['attributes'] ?? <Map<String, Object?>>{}) + as Map<String, Object?>; + for (final attributeEntry in attributes.entries) { + final key = attributeEntry.key; + final attribute = attributeEntry.value as Map<String, Object?>; + final relatedCollection = attribute['relatedCollection'] as String; + final relationType = attribute['relationType'] as String; + final side = attribute['side'] as String; + + if (!data.containsKey(key)) continue; + + final nestedModel = + "/databases/$databaseId/collections/$relatedCollection/documents"; + + if (relationType == 'oneToOne' || + (relationType == 'oneToMany' && side == 'child') || + (relationType == 'manyToOne' && side == 'parent')) { + // data[key] is a single document + String documentId = ''; + if (data[key] is String) { + // data[key] is a document ID + documentId = data[key] as String; + } else if (data[key] is Map<String, Object?>) { + // data[key] is a nested document + final related = data[key] as Map<String, Object?>; + documentId = (related['\$id'] ?? ID.unique()) as String; + await upsert(model: nestedModel, key: documentId, data: related); + } + data[key] = { + '\$databaseId': databaseId, + '\$collectionId': relatedCollection, + '\$id': documentId + }; + } else { + // data[key] is a list of documents + final result = <Map<String, Object?>>[]; + final relatedList = data[key] as List; + for (final related in relatedList) { + String documentId = ''; + if (related is String) { + // related is a document ID + documentId = related; + } else if (related is Map<String, Object?>) { + // related is a nested document + documentId = (related['\$id'] ?? ID.unique()) as String; + await upsert(model: nestedModel, key: documentId, data: related); + } + result.add({ + '\$databaseId': databaseId, + '\$collectionId': relatedCollection, + '\$id': documentId + }); + } + data[key] = result; + } + } + } - final recordRef = store.record(key); - final record = await recordRef.get(_db); - _cacheSize.update(oldData: record, newData: data); + final result = await _db.transaction((txn) async { + final store = getModelStore(model); + + final recordRef = store.record(key); + final oldData = await recordRef.get(txn); + final oldSize = oldData != null ? _cacheSize.encode(oldData).length : 0; - final result = await recordRef.put(_db, data, merge: true); + final result = await recordRef.put(txn, data, merge: true); + + final newSize = _cacheSize.encode(result).length; + final change = newSize - oldSize; + + await _cacheSize.applyChange(txn, change); + return result; + }); await _accessedAt.update(model: model, keys: [key]); return result; } @@ -167,16 +394,19 @@ class ModelData { Future<void> delete({required String model, required String key}) async { final store = getModelStore(model); RecordSnapshot<String, Map<String, Object?>>? record; + final recordRef = store.record(key); - record = await store.record(key).getSnapshot(_db); + record = await recordRef.getSnapshot(_db); if (record == null) { return; } - _cacheSize.update(oldData: record.value); - - await record.ref.delete(_db); + await _db.transaction((txn) async { + final oldSize = _cacheSize.encode(record!.value).length; + await _cacheSize.applyChange(txn, oldSize * -1); + await record.ref.delete(_db); + }); await _accessedAt.delete(model: model, key: record.key); } diff --git a/lib/src/offline_db_io.dart b/lib/src/offline_db_io.dart index 61b36422..d6cf5481 100644 --- a/lib/src/offline_db_io.dart +++ b/lib/src/offline_db_io.dart @@ -3,7 +3,7 @@ import 'dart:io'; import 'package:sembast/sembast.dart'; import 'package:sembast_sqflite/sembast_sqflite.dart'; import 'package:sqflite/sqflite.dart' as sqflite; -import 'package:sqflite_common_ffi/sqflite_ffi.dart'; +import 'package:sqflite_common_ffi/sqflite_ffi.dart' hide Database; class OfflineDatabase { static final OfflineDatabase instance = OfflineDatabase._internal(); diff --git a/lib/src/response.dart b/lib/src/response.dart index bf4eea3f..306bbff3 100644 --- a/lib/src/response.dart +++ b/lib/src/response.dart @@ -1,8 +1,9 @@ import 'dart:convert'; class Response<T> { - Response({this.data}); + Response({this.headers = const {}, this.data}); + final Map<String, String> headers; T? data; @override