From 900e7d671c64583e6e8b7d4ee881e3c567018e10 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 7 May 2026 11:10:48 +0530 Subject: [PATCH 1/3] chore: update React Native SDK to 0.30.0 --- .github/workflows/publish.yml | 4 ++-- CHANGELOG.md | 15 ++++++++++---- README.md | 2 +- package-lock.json | 38 +++++++++++++++++------------------ package.json | 5 +++-- src/client.ts | 21 +++++++++++++++++-- src/enums/o-auth-provider.ts | 3 +++ src/services/account.ts | 8 ++++---- 8 files changed, 62 insertions(+), 34 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index e6ea266e..47ba2484 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -14,10 +14,10 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Use Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: '24.14.1' registry-url: 'https://registry.npmjs.org' diff --git a/CHANGELOG.md b/CHANGELOG.md index cc1c3eb7..9b7fc0c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,18 @@ # Change log +## 0.30.0 + +* Added: Added `setCookie()` method to `Client` for forwarding incoming `Cookie` headers in server-side runtimes +* Added: Added `Fusionauth`, `Keycloak`, and `Kick` OAuth providers to `OAuthProvider` enum +* Updated: Updated `X-Appwrite-Response-Format` header to `1.9.4` +* Updated: Added `postcss` to dev dependencies + ## 0.29.0 -* Breaking: Added `subscribe` message flow for Realtime subscription updates. -* Breaking: Added `close()` support for Realtime subscriptions. -* Added: Added `subscriptions` metadata to Realtime events for targeted callbacks. -* Updated: Updated `X-Appwrite-Response-Format` header to `1.9.2`. +* Breaking: Added `subscribe` message flow for Realtime subscription updates +* Breaking: Added `close()` support for Realtime subscriptions +* Added: Added `subscriptions` metadata to Realtime events for targeted callbacks +* Updated: Updated `X-Appwrite-Response-Format` header to `1.9.2` ## 0.28.0 diff --git a/README.md b/README.md index b4092dd8..42d9e244 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Appwrite React Native SDK ![License](https://img.shields.io/github/license/appwrite/sdk-for-react-native.svg?style=flat-square) -![Version](https://img.shields.io/badge/api%20version-1.9.2-blue.svg?style=flat-square) +![Version](https://img.shields.io/badge/api%20version-1.9.4-blue.svg?style=flat-square) [![Build Status](https://img.shields.io/travis/com/appwrite/sdk-generator?style=flat-square)](https://travis-ci.com/appwrite/sdk-generator) [![Twitter Account](https://img.shields.io/twitter/follow/appwrite?color=00acee&label=twitter&style=flat-square)](https://twitter.com/appwrite) [![Discord](https://img.shields.io/discord/564160730845151244?label=discord&style=flat-square)](https://appwrite.io/discord) diff --git a/package-lock.json b/package-lock.json index ce385ce6..4359545b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "react-native-appwrite", - "version": "0.29.0", + "version": "0.30.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "react-native-appwrite", - "version": "0.29.0", + "version": "0.30.0", "license": "BSD-3-Clause", "dependencies": { "expo-file-system": "18.*.*", @@ -3450,9 +3450,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.10.21", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.21.tgz", - "integrity": "sha512-Q+rUQ7Uz8AHM7DEaNdwvfFCTq7a43lNTzuS94eiWqwyxfV/wJv+oUivef51T91mmRY4d4A1u9rcSvkeufCVXlA==", + "version": "2.10.23", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.23.tgz", + "integrity": "sha512-xwVXGqevyKPsiuQdLj+dZMVjidjJV508TBqexND5HrF89cGdCYCJFB3qhcxRHSeMctdCfbR1jrxBajhDy7o29g==", "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.cjs" @@ -3630,9 +3630,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001790", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001790.tgz", - "integrity": "sha512-bOoxfJPyYo+ds6W0YfptaCWbFnJYjh2Y1Eow5lRv+vI2u8ganPZqNm1JwNh0t2ELQCqIWg4B3dWEusgAmsoyOw==", + "version": "1.0.30001791", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001791.tgz", + "integrity": "sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ==", "funding": [ { "type": "opencollective", @@ -6213,13 +6213,13 @@ } }, "node_modules/plist": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", - "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.1.tgz", + "integrity": "sha512-ZIfcLJC+7E7FBFnDxm9MPmt7D+DidyQ26lewieO75AdhA2ayMtsJSES0iWzqJQbcVRSrTufQoy0DR94xHue0oA==", "license": "MIT", "peer": true, "dependencies": { - "@xmldom/xmldom": "^0.8.8", + "@xmldom/xmldom": "^0.9.10", "base64-js": "^1.5.1", "xmlbuilder": "^15.1.1" }, @@ -6238,9 +6238,9 @@ } }, "node_modules/postcss": { - "version": "8.4.49", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", - "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", + "version": "8.5.12", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz", + "integrity": "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==", "funding": [ { "type": "opencollective", @@ -6258,7 +6258,7 @@ "license": "MIT", "peer": true, "dependencies": { - "nanoid": "^3.3.7", + "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -7320,9 +7320,9 @@ } }, "node_modules/terser": { - "version": "5.46.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.1.tgz", - "integrity": "sha512-vzCjQO/rgUuK9sf8VJZvjqiqiHFaZLnOiimmUuOKODxWL8mm/xua7viT7aqX7dgPY60otQjUotzFMmCB4VdmqQ==", + "version": "5.46.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.2.tgz", + "integrity": "sha512-uxfo9fPcSgLDYob/w1FuL0c99MWiJDnv+5qXSQc5+Ki5NjVNsYi66INnMFBjf6uFz6OnX12piJQPF4IpjJTNTw==", "license": "BSD-2-Clause", "dependencies": { "@jridgewell/source-map": "^0.3.3", diff --git a/package.json b/package.json index d18d6fd1..b399bba3 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "react-native-appwrite", "homepage": "https://appwrite.io/support", "description": "Appwrite is an open-source self-hosted backend server that abstracts and simplifies complex and repetitive development tasks behind a very simple REST API", - "version": "0.29.0", + "version": "0.30.0", "license": "BSD-3-Clause", "main": "dist/cjs/sdk.js", "exports": { @@ -52,6 +52,7 @@ "glob": "^13.0.0", "rimraf": "^6.0.0", "@xmldom/xmldom": "^0.9.10", - "uuid": "^14.0.0" + "uuid": "^14.0.0", + "postcss": "^8.5.12" } } diff --git a/src/client.ts b/src/client.ts index a4a89430..9c032850 100644 --- a/src/client.ts +++ b/src/client.ts @@ -171,6 +171,7 @@ class Client { locale: '', session: '', devkey: '', + cookie: '', impersonateuserid: '', impersonateuseremail: '', impersonateuserphone: '', @@ -179,8 +180,8 @@ class Client { 'x-sdk-name': 'React Native', 'x-sdk-platform': 'client', 'x-sdk-language': 'reactnative', - 'x-sdk-version': '0.29.0', - 'X-Appwrite-Response-Format': '1.9.2', + 'x-sdk-version': '0.30.0', + 'X-Appwrite-Response-Format': '1.9.4', }; /** @@ -326,6 +327,22 @@ class Client { return this; } + /** + * Set Cookie + * + * The user cookie to authenticate with. Used by SDKs that forward an incoming + * Cookie header in server-side runtimes. + * + * @param value string + * + * @return {this} + */ + setCookie(value: string): this { + this.headers['Cookie'] = value; + this.config.cookie = value; + return this; + } + /** * Set ImpersonateUserId * diff --git a/src/enums/o-auth-provider.ts b/src/enums/o-auth-provider.ts index efc44844..cc9e340b 100644 --- a/src/enums/o-auth-provider.ts +++ b/src/enums/o-auth-provider.ts @@ -14,9 +14,12 @@ export enum OAuthProvider { Etsy = 'etsy', Facebook = 'facebook', Figma = 'figma', + Fusionauth = 'fusionauth', Github = 'github', Gitlab = 'gitlab', Google = 'google', + Keycloak = 'keycloak', + Kick = 'kick', Linkedin = 'linkedin', Microsoft = 'microsoft', Notion = 'notion', diff --git a/src/services/account.ts b/src/services/account.ts index 42c942d2..86be09e7 100644 --- a/src/services/account.ts +++ b/src/services/account.ts @@ -1632,7 +1632,7 @@ export class Account extends Service { * A user is limited to 10 active sessions at a time by default. [Learn more about session limits](https://appwrite.io/docs/authentication-security#limits). * * - * @param {OAuthProvider} params.provider - OAuth2 Provider. Currently, supported providers are: amazon, apple, auth0, authentik, autodesk, bitbucket, bitly, box, dailymotion, discord, disqus, dropbox, etsy, facebook, figma, github, gitlab, google, linkedin, microsoft, notion, oidc, okta, paypal, paypalSandbox, podio, salesforce, slack, spotify, stripe, tradeshift, tradeshiftBox, twitch, wordpress, x, yahoo, yammer, yandex, zoho, zoom. + * @param {OAuthProvider} params.provider - OAuth2 Provider. Currently, supported providers are: amazon, apple, auth0, authentik, autodesk, bitbucket, bitly, box, dailymotion, discord, disqus, dropbox, etsy, facebook, figma, fusionauth, github, gitlab, google, keycloak, kick, linkedin, microsoft, notion, oidc, okta, paypal, paypalSandbox, podio, salesforce, slack, spotify, stripe, tradeshift, tradeshiftBox, twitch, wordpress, x, yahoo, yammer, yandex, zoho, zoom. * @param {string} params.success - URL to redirect back to your app after a successful login attempt. Only URLs from hostnames in your project's platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API. * @param {string} params.failure - URL to redirect back to your app after a failed login attempt. Only URLs from hostnames in your project's platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API. * @param {string[]} params.scopes - A list of custom OAuth2 scopes. Check each provider internal docs for a list of supported scopes. Maximum of 100 scopes are allowed, each 4096 characters long. @@ -1648,7 +1648,7 @@ export class Account extends Service { * A user is limited to 10 active sessions at a time by default. [Learn more about session limits](https://appwrite.io/docs/authentication-security#limits). * * - * @param {OAuthProvider} provider - OAuth2 Provider. Currently, supported providers are: amazon, apple, auth0, authentik, autodesk, bitbucket, bitly, box, dailymotion, discord, disqus, dropbox, etsy, facebook, figma, github, gitlab, google, linkedin, microsoft, notion, oidc, okta, paypal, paypalSandbox, podio, salesforce, slack, spotify, stripe, tradeshift, tradeshiftBox, twitch, wordpress, x, yahoo, yammer, yandex, zoho, zoom. + * @param {OAuthProvider} provider - OAuth2 Provider. Currently, supported providers are: amazon, apple, auth0, authentik, autodesk, bitbucket, bitly, box, dailymotion, discord, disqus, dropbox, etsy, facebook, figma, fusionauth, github, gitlab, google, keycloak, kick, linkedin, microsoft, notion, oidc, okta, paypal, paypalSandbox, podio, salesforce, slack, spotify, stripe, tradeshift, tradeshiftBox, twitch, wordpress, x, yahoo, yammer, yandex, zoho, zoom. * @param {string} success - URL to redirect back to your app after a successful login attempt. Only URLs from hostnames in your project's platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API. * @param {string} failure - URL to redirect back to your app after a failed login attempt. Only URLs from hostnames in your project's platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API. * @param {string[]} scopes - A list of custom OAuth2 scopes. Check each provider internal docs for a list of supported scopes. Maximum of 100 scopes are allowed, each 4096 characters long. @@ -2323,7 +2323,7 @@ export class Account extends Service { * * A user is limited to 10 active sessions at a time by default. [Learn more about session limits](https://appwrite.io/docs/authentication-security#limits). * - * @param {OAuthProvider} params.provider - OAuth2 Provider. Currently, supported providers are: amazon, apple, auth0, authentik, autodesk, bitbucket, bitly, box, dailymotion, discord, disqus, dropbox, etsy, facebook, figma, github, gitlab, google, linkedin, microsoft, notion, oidc, okta, paypal, paypalSandbox, podio, salesforce, slack, spotify, stripe, tradeshift, tradeshiftBox, twitch, wordpress, x, yahoo, yammer, yandex, zoho, zoom. + * @param {OAuthProvider} params.provider - OAuth2 Provider. Currently, supported providers are: amazon, apple, auth0, authentik, autodesk, bitbucket, bitly, box, dailymotion, discord, disqus, dropbox, etsy, facebook, figma, fusionauth, github, gitlab, google, keycloak, kick, linkedin, microsoft, notion, oidc, okta, paypal, paypalSandbox, podio, salesforce, slack, spotify, stripe, tradeshift, tradeshiftBox, twitch, wordpress, x, yahoo, yammer, yandex, zoho, zoom. * @param {string} params.success - URL to redirect back to your app after a successful login attempt. Only URLs from hostnames in your project's platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API. * @param {string} params.failure - URL to redirect back to your app after a failed login attempt. Only URLs from hostnames in your project's platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API. * @param {string[]} params.scopes - A list of custom OAuth2 scopes. Check each provider internal docs for a list of supported scopes. Maximum of 100 scopes are allowed, each 4096 characters long. @@ -2338,7 +2338,7 @@ export class Account extends Service { * * A user is limited to 10 active sessions at a time by default. [Learn more about session limits](https://appwrite.io/docs/authentication-security#limits). * - * @param {OAuthProvider} provider - OAuth2 Provider. Currently, supported providers are: amazon, apple, auth0, authentik, autodesk, bitbucket, bitly, box, dailymotion, discord, disqus, dropbox, etsy, facebook, figma, github, gitlab, google, linkedin, microsoft, notion, oidc, okta, paypal, paypalSandbox, podio, salesforce, slack, spotify, stripe, tradeshift, tradeshiftBox, twitch, wordpress, x, yahoo, yammer, yandex, zoho, zoom. + * @param {OAuthProvider} provider - OAuth2 Provider. Currently, supported providers are: amazon, apple, auth0, authentik, autodesk, bitbucket, bitly, box, dailymotion, discord, disqus, dropbox, etsy, facebook, figma, fusionauth, github, gitlab, google, keycloak, kick, linkedin, microsoft, notion, oidc, okta, paypal, paypalSandbox, podio, salesforce, slack, spotify, stripe, tradeshift, tradeshiftBox, twitch, wordpress, x, yahoo, yammer, yandex, zoho, zoom. * @param {string} success - URL to redirect back to your app after a successful login attempt. Only URLs from hostnames in your project's platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API. * @param {string} failure - URL to redirect back to your app after a failed login attempt. Only URLs from hostnames in your project's platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API. * @param {string[]} scopes - A list of custom OAuth2 scopes. Check each provider internal docs for a list of supported scopes. Maximum of 100 scopes are allowed, each 4096 characters long. From 0da0a64167307ae7cb85916ce95bb90ace521608 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 18 May 2026 12:34:09 +0000 Subject: [PATCH 2/3] chore: update React Native SDK to 0.30.0 --- .github/workflows/publish.yml | 4 +- .npmrc | 1 + CHANGELOG.md | 10 +- README.md | 2 +- docs/examples/advisor/get-insight.md | 16 + docs/examples/advisor/get-report.md | 15 + docs/examples/advisor/list-insights.md | 17 + docs/examples/advisor/list-reports.md | 16 + docs/examples/functions/create-execution.md | 2 +- docs/examples/presences/delete.md | 15 + docs/examples/presences/get.md | 15 + docs/examples/presences/list.md | 17 + docs/examples/presences/update.md | 20 ++ docs/examples/presences/upsert.md | 19 ++ src/channel.ts | 15 +- src/client.ts | 2 +- src/index.ts | 2 + src/models.ts | 235 ++++++++++++++ src/services/advisor.ts | 234 +++++++++++++ src/services/presences.ts | 343 ++++++++++++++++++++ src/services/realtime.ts | 162 ++++++++- 21 files changed, 1138 insertions(+), 24 deletions(-) create mode 100644 .npmrc create mode 100644 docs/examples/advisor/get-insight.md create mode 100644 docs/examples/advisor/get-report.md create mode 100644 docs/examples/advisor/list-insights.md create mode 100644 docs/examples/advisor/list-reports.md create mode 100644 docs/examples/presences/delete.md create mode 100644 docs/examples/presences/get.md create mode 100644 docs/examples/presences/list.md create mode 100644 docs/examples/presences/update.md create mode 100644 docs/examples/presences/upsert.md create mode 100644 src/services/advisor.ts create mode 100644 src/services/presences.ts diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 47ba2484..ee63d526 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -14,10 +14,10 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Use Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: '24.14.1' registry-url: 'https://registry.npmjs.org' diff --git a/.npmrc b/.npmrc new file mode 100644 index 00000000..7253a5ce --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +min-release-age=7 diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b7fc0c0..3ac51798 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,10 +2,12 @@ ## 0.30.0 -* Added: Added `setCookie()` method to `Client` for forwarding incoming `Cookie` headers in server-side runtimes -* Added: Added `Fusionauth`, `Keycloak`, and `Kick` OAuth providers to `OAuthProvider` enum -* Updated: Updated `X-Appwrite-Response-Format` header to `1.9.4` -* Updated: Added `postcss` to dev dependencies +* Added: Realtime `presences` channel and `RealtimePresence` types for presence subscriptions +* Added: `Advisor` and `Presences` services +* Added: `Insight`, `Presence`, and `Report` models with list variants +* Added: `fusionauth`, `keycloak`, and `kick` providers to `OAuthProvider` enum +* Added: `Client.setCookie()` method for forwarding cookies in server-side runtimes +* Updated: `X-Appwrite-Response-Format` header to `1.9.5` ## 0.29.0 diff --git a/README.md b/README.md index 42d9e244..70321e6a 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Appwrite React Native SDK ![License](https://img.shields.io/github/license/appwrite/sdk-for-react-native.svg?style=flat-square) -![Version](https://img.shields.io/badge/api%20version-1.9.4-blue.svg?style=flat-square) +![Version](https://img.shields.io/badge/api%20version-1.9.5-blue.svg?style=flat-square) [![Build Status](https://img.shields.io/travis/com/appwrite/sdk-generator?style=flat-square)](https://travis-ci.com/appwrite/sdk-generator) [![Twitter Account](https://img.shields.io/twitter/follow/appwrite?color=00acee&label=twitter&style=flat-square)](https://twitter.com/appwrite) [![Discord](https://img.shields.io/discord/564160730845151244?label=discord&style=flat-square)](https://appwrite.io/discord) diff --git a/docs/examples/advisor/get-insight.md b/docs/examples/advisor/get-insight.md new file mode 100644 index 00000000..0f2a45b4 --- /dev/null +++ b/docs/examples/advisor/get-insight.md @@ -0,0 +1,16 @@ +```javascript +import { Client, Advisor } from "react-native-appwrite"; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject(''); // Your project ID + +const advisor = new Advisor(client); + +const result = await advisor.getInsight({ + reportId: '', + insightId: '' +}); + +console.log(result); +``` diff --git a/docs/examples/advisor/get-report.md b/docs/examples/advisor/get-report.md new file mode 100644 index 00000000..e2dbdf27 --- /dev/null +++ b/docs/examples/advisor/get-report.md @@ -0,0 +1,15 @@ +```javascript +import { Client, Advisor } from "react-native-appwrite"; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject(''); // Your project ID + +const advisor = new Advisor(client); + +const result = await advisor.getReport({ + reportId: '' +}); + +console.log(result); +``` diff --git a/docs/examples/advisor/list-insights.md b/docs/examples/advisor/list-insights.md new file mode 100644 index 00000000..94bb145f --- /dev/null +++ b/docs/examples/advisor/list-insights.md @@ -0,0 +1,17 @@ +```javascript +import { Client, Advisor } from "react-native-appwrite"; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject(''); // Your project ID + +const advisor = new Advisor(client); + +const result = await advisor.listInsights({ + reportId: '', + queries: [], // optional + total: false // optional +}); + +console.log(result); +``` diff --git a/docs/examples/advisor/list-reports.md b/docs/examples/advisor/list-reports.md new file mode 100644 index 00000000..2194ba57 --- /dev/null +++ b/docs/examples/advisor/list-reports.md @@ -0,0 +1,16 @@ +```javascript +import { Client, Advisor } from "react-native-appwrite"; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject(''); // Your project ID + +const advisor = new Advisor(client); + +const result = await advisor.listReports({ + queries: [], // optional + total: false // optional +}); + +console.log(result); +``` diff --git a/docs/examples/functions/create-execution.md b/docs/examples/functions/create-execution.md index 015bbedd..3a05c923 100644 --- a/docs/examples/functions/create-execution.md +++ b/docs/examples/functions/create-execution.md @@ -11,7 +11,7 @@ const result = await functions.createExecution({ functionId: '', body: '', // optional async: false, // optional - path: '', // optional + xpath: '', // optional method: ExecutionMethod.GET, // optional headers: {}, // optional scheduledAt: '' // optional diff --git a/docs/examples/presences/delete.md b/docs/examples/presences/delete.md new file mode 100644 index 00000000..447de030 --- /dev/null +++ b/docs/examples/presences/delete.md @@ -0,0 +1,15 @@ +```javascript +import { Client, Presences } from "react-native-appwrite"; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject(''); // Your project ID + +const presences = new Presences(client); + +const result = await presences.delete({ + presenceId: '' +}); + +console.log(result); +``` diff --git a/docs/examples/presences/get.md b/docs/examples/presences/get.md new file mode 100644 index 00000000..e5b89718 --- /dev/null +++ b/docs/examples/presences/get.md @@ -0,0 +1,15 @@ +```javascript +import { Client, Presences } from "react-native-appwrite"; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject(''); // Your project ID + +const presences = new Presences(client); + +const result = await presences.get({ + presenceId: '' +}); + +console.log(result); +``` diff --git a/docs/examples/presences/list.md b/docs/examples/presences/list.md new file mode 100644 index 00000000..ccfe7079 --- /dev/null +++ b/docs/examples/presences/list.md @@ -0,0 +1,17 @@ +```javascript +import { Client, Presences } from "react-native-appwrite"; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject(''); // Your project ID + +const presences = new Presences(client); + +const result = await presences.list({ + queries: [], // optional + total: false, // optional + ttl: 0 // optional +}); + +console.log(result); +``` diff --git a/docs/examples/presences/update.md b/docs/examples/presences/update.md new file mode 100644 index 00000000..16ef7428 --- /dev/null +++ b/docs/examples/presences/update.md @@ -0,0 +1,20 @@ +```javascript +import { Client, Presences, Permission, Role } from "react-native-appwrite"; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject(''); // Your project ID + +const presences = new Presences(client); + +const result = await presences.update({ + presenceId: '', + status: '', // optional + expiresAt: '2020-10-15T06:38:00.000+00:00', // optional + metadata: {}, // optional + permissions: ["read("any")"], // optional + purge: false // optional +}); + +console.log(result); +``` diff --git a/docs/examples/presences/upsert.md b/docs/examples/presences/upsert.md new file mode 100644 index 00000000..522c44e0 --- /dev/null +++ b/docs/examples/presences/upsert.md @@ -0,0 +1,19 @@ +```javascript +import { Client, Presences, Permission, Role } from "react-native-appwrite"; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject(''); // Your project ID + +const presences = new Presences(client); + +const result = await presences.upsert({ + presenceId: '', + status: '', + permissions: ["read("any")"], // optional + expiresAt: '2020-10-15T06:38:00.000+00:00', // optional + metadata: {} // optional +}); + +console.log(result); +``` diff --git a/src/channel.ts b/src/channel.ts index b6e41621..653bd313 100644 --- a/src/channel.ts +++ b/src/channel.ts @@ -11,9 +11,10 @@ interface Func { _fn: any } interface Execution { _exec: any } interface Team { _team: any } interface Membership { _mem: any } +interface Presence { _presence: any } interface Resolved { _res: any } -type Actionable = Document | Row | File | Team | Membership; +type Actionable = Document | Row | File | Team | Membership | Presence; function normalize(id: string): string { if (id === undefined || id === null) { @@ -79,7 +80,7 @@ export class Channel { return this.resolve("create"); } - upsert(this: Channel): Channel { + upsert(this: Channel): Channel { return this.resolve("upsert"); } @@ -120,6 +121,10 @@ export class Channel { return new Channel(["memberships", normalize(id)]); } + static presence(id: string) { + return new Channel(["presences", normalize(id)]); + } + static account(): string { return "account"; } @@ -148,8 +153,12 @@ export class Channel { static memberships(): string { return "memberships"; } + + static presences(): string { + return "presences"; + } } // Export types for backward compatibility with realtime -export type ActionableChannel = Channel | Channel | Channel | Channel | Channel | Channel; +export type ActionableChannel = Channel | Channel | Channel | Channel | Channel | Channel | Channel; export type ResolvedChannel = Channel; diff --git a/src/client.ts b/src/client.ts index 9c032850..744509a8 100644 --- a/src/client.ts +++ b/src/client.ts @@ -181,7 +181,7 @@ class Client { 'x-sdk-platform': 'client', 'x-sdk-language': 'reactnative', 'x-sdk-version': '0.30.0', - 'X-Appwrite-Response-Format': '1.9.4', + 'X-Appwrite-Response-Format': '1.9.5', }; /** diff --git a/src/index.ts b/src/index.ts index 3769d3c3..0b86dac4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,6 +6,8 @@ export { Functions } from './services/functions'; export { Graphql } from './services/graphql'; export { Locale } from './services/locale'; export { Messaging } from './services/messaging'; +export { Presences } from './services/presences'; +export { Advisor } from './services/advisor'; export { Storage } from './services/storage'; export { TablesDB } from './services/tables-db'; export { Teams } from './services/teams'; diff --git a/src/models.ts b/src/models.ts index 16c1d8e9..c87bb866 100644 --- a/src/models.ts +++ b/src/models.ts @@ -33,6 +33,20 @@ export namespace Models { documents: Document[]; } + /** + * Presences List + */ + export type PresenceList = { + /** + * Total number of presences that matched your query. + */ + total: number; + /** + * List of presences. + */ + presences: Presence[]; + } + /** * Sessions List */ @@ -229,6 +243,34 @@ export namespace Models { transactions: Transaction[]; } + /** + * Insights List + */ + export type InsightList = { + /** + * Total number of insights that matched your query. + */ + total: number; + /** + * List of insights. + */ + insights: Insight[]; + } + + /** + * Reports List + */ + export type ReportList = { + /** + * Total number of reports that matched your query. + */ + total: number; + /** + * List of reports. + */ + reports: Report[]; + } + /** * Row */ @@ -307,6 +349,49 @@ export namespace Models { [__default]: true; }; + /** + * Presence + */ + export type Presence = { + /** + * Presence ID. + */ + $id: string; + /** + * Presence creation date in ISO 8601 format. + */ + $createdAt: string; + /** + * Presence update date in ISO 8601 format. + */ + $updatedAt: string; + /** + * Presence permissions. [Learn more about permissions](https://appwrite.io/docs/permissions). + */ + $permissions: string[]; + /** + * User ID. + */ + userId: string; + /** + * Presence status. + */ + status?: string; + /** + * Presence source. + */ + source: string; + /** + * Presence expiry date in ISO 8601 format. + */ + expiresAt?: string; + } + + export type DefaultPresence = Presence & { + [key: string]: any; + [__default]: true; + }; + /** * Log */ @@ -1389,4 +1474,154 @@ export namespace Models { */ expired: boolean; } + + /** + * Insight + */ + export type Insight = { + /** + * Insight ID. + */ + $id: string; + /** + * Insight creation date in ISO 8601 format. + */ + $createdAt: string; + /** + * Insight update date in ISO 8601 format. + */ + $updatedAt: string; + /** + * Parent report ID. Insights always belong to a report. + */ + reportId: string; + /** + * Insight type. One of databaseIndex (legacy), tablesDBIndex, documentsDBIndex, vectorsDBIndex, databasePerformance, sitePerformance, siteAccessibility, siteSeo, functionPerformance. The index types are engine-specific so each CTA can pair the right service+method (databases.createIndex, tablesDB.createIndex, documentsDB.createIndex, or vectorsDB.createIndex). + */ + type: string; + /** + * Insight severity. One of info, warning, critical. + */ + severity: string; + /** + * Insight status. One of active, dismissed. + */ + status: string; + /** + * Type of the resource the insight is about. Plural noun, e.g. databases, sites, functions. + */ + resourceType: string; + /** + * ID of the resource the insight is about. + */ + resourceId: string; + /** + * Plural noun for the parent resource that contains the insight's resource, e.g. an insight about a column index on a table → resourceType=indexes, parentResourceType=tables. Empty when the resource has no parent. + */ + parentResourceType: string; + /** + * ID of the parent resource. Empty when the resource has no parent. + */ + parentResourceId: string; + /** + * Insight title. + */ + title: string; + /** + * Short markdown summary describing the insight. + */ + summary: string; + /** + * List of call-to-action buttons attached to this insight. + */ + ctas: InsightCTA[]; + /** + * Time the insight was analyzed in ISO 8601 format. + */ + analyzedAt?: string; + /** + * Time the insight was dismissed in ISO 8601 format. Empty when not dismissed. + */ + dismissedAt?: string; + /** + * User ID that dismissed the insight. Empty when not dismissed. + */ + dismissedBy?: string; + } + + /** + * InsightCTA + */ + export type InsightCTA = { + /** + * Human-readable label for the CTA, used in UI. + */ + label: string; + /** + * Public API service (SDK namespace) the client should invoke. Must match the engine that owns the resource — for index suggestions: databases (legacy), tablesDB, documentsDB, or vectorsDB. + */ + service: string; + /** + * Public API method on the chosen service the client should invoke when this CTA is triggered. + */ + method: string; + /** + * Parameter map the client should pass to the service method when this CTA is triggered. Keys match the target API's parameter names (e.g. databaseId/tableId/columns for tablesDB, databaseId/collectionId/attributes for the legacy Databases API). + */ + params: object; + } + + /** + * Report + */ + export type Report = { + /** + * Report ID. + */ + $id: string; + /** + * Report creation date in ISO 8601 format. + */ + $createdAt: string; + /** + * Report update date in ISO 8601 format. + */ + $updatedAt: string; + /** + * ID of the third-party app that submitted the report. + */ + appId: string; + /** + * Analyzer that produced this report. e.g. lighthouse, audit, databaseAnalyzer. + */ + type: string; + /** + * Short, human-readable title for the report. + */ + title: string; + /** + * Markdown summary describing the report. + */ + summary: string; + /** + * Plural noun describing what the report analyzes, e.g. databases, sites, urls. + */ + targetType: string; + /** + * Free-form target identifier (URL for lighthouse, resource ID for db). + */ + target: string; + /** + * Categories covered by the report, e.g. performance, accessibility. + */ + categories: string[]; + /** + * Insights nested under this report. + */ + insights: Insight[]; + /** + * Time the report was analyzed in ISO 8601 format. + */ + analyzedAt?: string; + } } diff --git a/src/services/advisor.ts b/src/services/advisor.ts new file mode 100644 index 00000000..6407d51b --- /dev/null +++ b/src/services/advisor.ts @@ -0,0 +1,234 @@ +import { Service } from '../service'; +import { AppwriteException, Client } from '../client'; +import type { Models } from '../models'; +import type { UploadProgress, Payload } from '../client'; +import * as FileSystem from 'expo-file-system'; +import { Platform as RNPlatform } from 'react-native'; + + +export class Advisor extends Service { + + constructor(client: Client) + { + super(client); + } + + /** + * Get a list of all the project's analyzer reports. You can use the query params to filter your results. + * + * + * @param {string[]} params.queries - Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of 100 queries are allowed, each 4096 characters long. You may filter on the following attributes: appId, type, targetType, target, analyzedAt + * @param {boolean} params.total - When set to false, the total count returned will be 0 and will not be calculated. + * @throws {AppwriteException} + * @returns {Promise} + */ + listReports(params?: { queries?: string[], total?: boolean }): Promise; + /** + * Get a list of all the project's analyzer reports. You can use the query params to filter your results. + * + * + * @param {string[]} queries - Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of 100 queries are allowed, each 4096 characters long. You may filter on the following attributes: appId, type, targetType, target, analyzedAt + * @param {boolean} total - When set to false, the total count returned will be 0 and will not be calculated. + * @throws {AppwriteException} + * @returns {Promise} + * @deprecated Use the object parameter style method for a better developer experience. + */ + listReports(queries?: string[], total?: boolean): Promise; + listReports( + paramsOrFirst?: { queries?: string[], total?: boolean } | string[], + ...rest: [(boolean)?] + ): Promise { + let params: { queries?: string[], total?: boolean }; + + if (!paramsOrFirst || (paramsOrFirst && typeof paramsOrFirst === 'object' && !Array.isArray(paramsOrFirst))) { + params = (paramsOrFirst || {}) as { queries?: string[], total?: boolean }; + } else { + params = { + queries: paramsOrFirst as string[], + total: rest[0] as boolean + }; + } + + const queries = params.queries; + const total = params.total; + + const apiPath = '/reports'; + const payload: Payload = {}; + + if (typeof queries !== 'undefined') { + payload['queries'] = queries; + } + + if (typeof total !== 'undefined') { + payload['total'] = total; + } + + const uri = new URL(this.client.config.endpoint + apiPath); + return this.client.call('get', uri, { + }, payload); + } + + /** + * Get an analyzer report by its unique ID. The response includes the report's metadata and the nested insights it produced. + * + * + * @param {string} params.reportId - Report ID. + * @throws {AppwriteException} + * @returns {Promise} + */ + getReport(params: { reportId: string }): Promise; + /** + * Get an analyzer report by its unique ID. The response includes the report's metadata and the nested insights it produced. + * + * + * @param {string} reportId - Report ID. + * @throws {AppwriteException} + * @returns {Promise} + * @deprecated Use the object parameter style method for a better developer experience. + */ + getReport(reportId: string): Promise; + getReport( + paramsOrFirst: { reportId: string } | string + ): Promise { + let params: { reportId: string }; + + if ((paramsOrFirst && typeof paramsOrFirst === 'object' && !Array.isArray(paramsOrFirst))) { + params = (paramsOrFirst || {}) as { reportId: string }; + } else { + params = { + reportId: paramsOrFirst as string + }; + } + + const reportId = params.reportId; + + if (typeof reportId === 'undefined') { + throw new AppwriteException('Missing required parameter: "reportId"'); + } + + const apiPath = '/reports/{reportId}'.replace('{reportId}', reportId); + const payload: Payload = {}; + + const uri = new URL(this.client.config.endpoint + apiPath); + return this.client.call('get', uri, { + }, payload); + } + + /** + * List the insights produced under a single analyzer report. You can use the query params to filter your results further. + * + * + * @param {string} params.reportId - Parent report ID. + * @param {string[]} params.queries - Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of 100 queries are allowed, each 4096 characters long. You may filter on the following attributes: type, severity, status, resourceType, resourceId, parentResourceType, parentResourceId, analyzedAt, dismissedAt, dismissedBy + * @param {boolean} params.total - When set to false, the total count returned will be 0 and will not be calculated. + * @throws {AppwriteException} + * @returns {Promise} + */ + listInsights(params: { reportId: string, queries?: string[], total?: boolean }): Promise; + /** + * List the insights produced under a single analyzer report. You can use the query params to filter your results further. + * + * + * @param {string} reportId - Parent report ID. + * @param {string[]} queries - Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of 100 queries are allowed, each 4096 characters long. You may filter on the following attributes: type, severity, status, resourceType, resourceId, parentResourceType, parentResourceId, analyzedAt, dismissedAt, dismissedBy + * @param {boolean} total - When set to false, the total count returned will be 0 and will not be calculated. + * @throws {AppwriteException} + * @returns {Promise} + * @deprecated Use the object parameter style method for a better developer experience. + */ + listInsights(reportId: string, queries?: string[], total?: boolean): Promise; + listInsights( + paramsOrFirst: { reportId: string, queries?: string[], total?: boolean } | string, + ...rest: [(string[])?, (boolean)?] + ): Promise { + let params: { reportId: string, queries?: string[], total?: boolean }; + + if ((paramsOrFirst && typeof paramsOrFirst === 'object' && !Array.isArray(paramsOrFirst))) { + params = (paramsOrFirst || {}) as { reportId: string, queries?: string[], total?: boolean }; + } else { + params = { + reportId: paramsOrFirst as string, + queries: rest[0] as string[], + total: rest[1] as boolean + }; + } + + const reportId = params.reportId; + const queries = params.queries; + const total = params.total; + + if (typeof reportId === 'undefined') { + throw new AppwriteException('Missing required parameter: "reportId"'); + } + + const apiPath = '/reports/{reportId}/insights'.replace('{reportId}', reportId); + const payload: Payload = {}; + + if (typeof queries !== 'undefined') { + payload['queries'] = queries; + } + + if (typeof total !== 'undefined') { + payload['total'] = total; + } + + const uri = new URL(this.client.config.endpoint + apiPath); + return this.client.call('get', uri, { + }, payload); + } + + /** + * Get an insight by its unique ID, scoped to its parent report. + * + * + * @param {string} params.reportId - Parent report ID. + * @param {string} params.insightId - Insight ID. + * @throws {AppwriteException} + * @returns {Promise} + */ + getInsight(params: { reportId: string, insightId: string }): Promise; + /** + * Get an insight by its unique ID, scoped to its parent report. + * + * + * @param {string} reportId - Parent report ID. + * @param {string} insightId - Insight ID. + * @throws {AppwriteException} + * @returns {Promise} + * @deprecated Use the object parameter style method for a better developer experience. + */ + getInsight(reportId: string, insightId: string): Promise; + getInsight( + paramsOrFirst: { reportId: string, insightId: string } | string, + ...rest: [(string)?] + ): Promise { + let params: { reportId: string, insightId: string }; + + if ((paramsOrFirst && typeof paramsOrFirst === 'object' && !Array.isArray(paramsOrFirst))) { + params = (paramsOrFirst || {}) as { reportId: string, insightId: string }; + } else { + params = { + reportId: paramsOrFirst as string, + insightId: rest[0] as string + }; + } + + const reportId = params.reportId; + const insightId = params.insightId; + + if (typeof reportId === 'undefined') { + throw new AppwriteException('Missing required parameter: "reportId"'); + } + + if (typeof insightId === 'undefined') { + throw new AppwriteException('Missing required parameter: "insightId"'); + } + + const apiPath = '/reports/{reportId}/insights/{insightId}'.replace('{reportId}', reportId).replace('{insightId}', insightId); + const payload: Payload = {}; + + const uri = new URL(this.client.config.endpoint + apiPath); + return this.client.call('get', uri, { + }, payload); + } +}; diff --git a/src/services/presences.ts b/src/services/presences.ts new file mode 100644 index 00000000..acf920a8 --- /dev/null +++ b/src/services/presences.ts @@ -0,0 +1,343 @@ +import { Service } from '../service'; +import { AppwriteException, Client } from '../client'; +import type { Models } from '../models'; +import type { UploadProgress, Payload } from '../client'; +import * as FileSystem from 'expo-file-system'; +import { Platform as RNPlatform } from 'react-native'; + + +export class Presences extends Service { + + constructor(client: Client) + { + super(client); + } + + /** + * List presence logs. Expired entries are filtered out automatically. + * + * + * @param {string[]} params.queries - Array of query strings generated using the Query class provided by the SDK. + * @param {boolean} params.total - When set to false, the total count returned will be 0 and will not be calculated. + * @param {number} params.ttl - TTL (seconds) for caching list responses. Responses are stored in an in-memory key-value cache, keyed per project, collection, schema version (attributes and indexes), caller authorization roles, and the exact query — so users with different permissions never share cached entries. Schema changes invalidate cached entries automatically; document writes do not, so choose a TTL you are comfortable serving as stale data. Set to 0 to disable caching. Must be between 0 and 86400 (24 hours). + * @throws {AppwriteException} + * @returns {Promise} + */ + list(params?: { queries?: string[], total?: boolean, ttl?: number }): Promise>; + /** + * List presence logs. Expired entries are filtered out automatically. + * + * + * @param {string[]} queries - Array of query strings generated using the Query class provided by the SDK. + * @param {boolean} total - When set to false, the total count returned will be 0 and will not be calculated. + * @param {number} ttl - TTL (seconds) for caching list responses. Responses are stored in an in-memory key-value cache, keyed per project, collection, schema version (attributes and indexes), caller authorization roles, and the exact query — so users with different permissions never share cached entries. Schema changes invalidate cached entries automatically; document writes do not, so choose a TTL you are comfortable serving as stale data. Set to 0 to disable caching. Must be between 0 and 86400 (24 hours). + * @throws {AppwriteException} + * @returns {Promise>} + * @deprecated Use the object parameter style method for a better developer experience. + */ + list(queries?: string[], total?: boolean, ttl?: number): Promise>; + list( + paramsOrFirst?: { queries?: string[], total?: boolean, ttl?: number } | string[], + ...rest: [(boolean)?, (number)?] + ): Promise> { + let params: { queries?: string[], total?: boolean, ttl?: number }; + + if (!paramsOrFirst || (paramsOrFirst && typeof paramsOrFirst === 'object' && !Array.isArray(paramsOrFirst))) { + params = (paramsOrFirst || {}) as { queries?: string[], total?: boolean, ttl?: number }; + } else { + params = { + queries: paramsOrFirst as string[], + total: rest[0] as boolean, + ttl: rest[1] as number + }; + } + + const queries = params.queries; + const total = params.total; + const ttl = params.ttl; + + const apiPath = '/presences'; + const payload: Payload = {}; + + if (typeof queries !== 'undefined') { + payload['queries'] = queries; + } + + if (typeof total !== 'undefined') { + payload['total'] = total; + } + + if (typeof ttl !== 'undefined') { + payload['ttl'] = ttl; + } + + const uri = new URL(this.client.config.endpoint + apiPath); + return this.client.call('get', uri, { + }, payload); + } + + /** + * Get a presence log by its unique ID. Entries whose `expiresAt` is in the past are treated as not found. + * + * + * @param {string} params.presenceId - Presence unique ID. + * @throws {AppwriteException} + * @returns {Promise} + */ + get(params: { presenceId: string }): Promise; + /** + * Get a presence log by its unique ID. Entries whose `expiresAt` is in the past are treated as not found. + * + * + * @param {string} presenceId - Presence unique ID. + * @throws {AppwriteException} + * @returns {Promise} + * @deprecated Use the object parameter style method for a better developer experience. + */ + get(presenceId: string): Promise; + get( + paramsOrFirst: { presenceId: string } | string + ): Promise { + let params: { presenceId: string }; + + if ((paramsOrFirst && typeof paramsOrFirst === 'object' && !Array.isArray(paramsOrFirst))) { + params = (paramsOrFirst || {}) as { presenceId: string }; + } else { + params = { + presenceId: paramsOrFirst as string + }; + } + + const presenceId = params.presenceId; + + if (typeof presenceId === 'undefined') { + throw new AppwriteException('Missing required parameter: "presenceId"'); + } + + const apiPath = '/presences/{presenceId}'.replace('{presenceId}', presenceId); + const payload: Payload = {}; + + const uri = new URL(this.client.config.endpoint + apiPath); + return this.client.call('get', uri, { + }, payload); + } + + /** + * Create or update a presence log by its user ID. + * + * + * @param {string} params.presenceId - Presence unique ID. + * @param {string} params.status - Presence status. + * @param {string[]} params.permissions - An array of permissions strings. By default, only the current user is granted all permissions. [Learn more about permissions](https://appwrite.io/docs/permissions). + * @param {string} params.expiresAt - Presence expiry datetime. + * @param {object} params.metadata - Presence metadata object. + * @throws {AppwriteException} + * @returns {Promise} + */ + upsert(params: { presenceId: string, status: string, permissions?: string[], expiresAt?: string, metadata?: object }): Promise; + /** + * Create or update a presence log by its user ID. + * + * + * @param {string} presenceId - Presence unique ID. + * @param {string} status - Presence status. + * @param {string[]} permissions - An array of permissions strings. By default, only the current user is granted all permissions. [Learn more about permissions](https://appwrite.io/docs/permissions). + * @param {string} expiresAt - Presence expiry datetime. + * @param {object} metadata - Presence metadata object. + * @throws {AppwriteException} + * @returns {Promise} + * @deprecated Use the object parameter style method for a better developer experience. + */ + upsert(presenceId: string, status: string, permissions?: string[], expiresAt?: string, metadata?: object): Promise; + upsert( + paramsOrFirst: { presenceId: string, status: string, permissions?: string[], expiresAt?: string, metadata?: object } | string, + ...rest: [(string)?, (string[])?, (string)?, (object)?] + ): Promise { + let params: { presenceId: string, status: string, permissions?: string[], expiresAt?: string, metadata?: object }; + + if ((paramsOrFirst && typeof paramsOrFirst === 'object' && !Array.isArray(paramsOrFirst))) { + params = (paramsOrFirst || {}) as { presenceId: string, status: string, permissions?: string[], expiresAt?: string, metadata?: object }; + } else { + params = { + presenceId: paramsOrFirst as string, + status: rest[0] as string, + permissions: rest[1] as string[], + expiresAt: rest[2] as string, + metadata: rest[3] as object + }; + } + + const presenceId = params.presenceId; + const status = params.status; + const permissions = params.permissions; + const expiresAt = params.expiresAt; + const metadata = params.metadata; + + if (typeof presenceId === 'undefined') { + throw new AppwriteException('Missing required parameter: "presenceId"'); + } + + if (typeof status === 'undefined') { + throw new AppwriteException('Missing required parameter: "status"'); + } + + const apiPath = '/presences/{presenceId}'.replace('{presenceId}', presenceId); + const payload: Payload = {}; + + if (typeof status !== 'undefined') { + payload['status'] = status; + } + + if (typeof permissions !== 'undefined') { + payload['permissions'] = permissions; + } + + if (typeof expiresAt !== 'undefined') { + payload['expiresAt'] = expiresAt; + } + + if (typeof metadata !== 'undefined') { + payload['metadata'] = metadata; + } + + const uri = new URL(this.client.config.endpoint + apiPath); + return this.client.call('put', uri, { + 'content-type': 'application/json', + }, payload); + } + + /** + * Update a presence log by its unique ID. Using the patch method you can pass only specific fields that will get updated. + * + * + * @param {string} params.presenceId - Presence unique ID. + * @param {string} params.status - Presence status. + * @param {string} params.expiresAt - Presence expiry datetime. + * @param {object} params.metadata - Presence metadata object. + * @param {string[]} params.permissions - An array of permissions strings. By default, only the current user is granted all permissions. [Learn more about permissions](https://appwrite.io/docs/permissions). + * @param {boolean} params.purge - When true, purge cached responses used by list presences endpoint. + * @throws {AppwriteException} + * @returns {Promise} + */ + update(params: { presenceId: string, status?: string, expiresAt?: string, metadata?: object, permissions?: string[], purge?: boolean }): Promise; + /** + * Update a presence log by its unique ID. Using the patch method you can pass only specific fields that will get updated. + * + * + * @param {string} presenceId - Presence unique ID. + * @param {string} status - Presence status. + * @param {string} expiresAt - Presence expiry datetime. + * @param {object} metadata - Presence metadata object. + * @param {string[]} permissions - An array of permissions strings. By default, only the current user is granted all permissions. [Learn more about permissions](https://appwrite.io/docs/permissions). + * @param {boolean} purge - When true, purge cached responses used by list presences endpoint. + * @throws {AppwriteException} + * @returns {Promise} + * @deprecated Use the object parameter style method for a better developer experience. + */ + update(presenceId: string, status?: string, expiresAt?: string, metadata?: object, permissions?: string[], purge?: boolean): Promise; + update( + paramsOrFirst: { presenceId: string, status?: string, expiresAt?: string, metadata?: object, permissions?: string[], purge?: boolean } | string, + ...rest: [(string)?, (string)?, (object)?, (string[])?, (boolean)?] + ): Promise { + let params: { presenceId: string, status?: string, expiresAt?: string, metadata?: object, permissions?: string[], purge?: boolean }; + + if ((paramsOrFirst && typeof paramsOrFirst === 'object' && !Array.isArray(paramsOrFirst))) { + params = (paramsOrFirst || {}) as { presenceId: string, status?: string, expiresAt?: string, metadata?: object, permissions?: string[], purge?: boolean }; + } else { + params = { + presenceId: paramsOrFirst as string, + status: rest[0] as string, + expiresAt: rest[1] as string, + metadata: rest[2] as object, + permissions: rest[3] as string[], + purge: rest[4] as boolean + }; + } + + const presenceId = params.presenceId; + const status = params.status; + const expiresAt = params.expiresAt; + const metadata = params.metadata; + const permissions = params.permissions; + const purge = params.purge; + + if (typeof presenceId === 'undefined') { + throw new AppwriteException('Missing required parameter: "presenceId"'); + } + + const apiPath = '/presences/{presenceId}'.replace('{presenceId}', presenceId); + const payload: Payload = {}; + + if (typeof status !== 'undefined') { + payload['status'] = status; + } + + if (typeof expiresAt !== 'undefined') { + payload['expiresAt'] = expiresAt; + } + + if (typeof metadata !== 'undefined') { + payload['metadata'] = metadata; + } + + if (typeof permissions !== 'undefined') { + payload['permissions'] = permissions; + } + + if (typeof purge !== 'undefined') { + payload['purge'] = purge; + } + + const uri = new URL(this.client.config.endpoint + apiPath); + return this.client.call('patch', uri, { + 'content-type': 'application/json', + }, payload); + } + + /** + * Delete a presence log by its unique ID. + * + * + * @param {string} params.presenceId - Presence unique ID. + * @throws {AppwriteException} + * @returns {Promise} + */ + delete(params: { presenceId: string }): Promise<{}>; + /** + * Delete a presence log by its unique ID. + * + * + * @param {string} presenceId - Presence unique ID. + * @throws {AppwriteException} + * @returns {Promise<{}>} + * @deprecated Use the object parameter style method for a better developer experience. + */ + delete(presenceId: string): Promise<{}>; + delete( + paramsOrFirst: { presenceId: string } | string + ): Promise<{}> { + let params: { presenceId: string }; + + if ((paramsOrFirst && typeof paramsOrFirst === 'object' && !Array.isArray(paramsOrFirst))) { + params = (paramsOrFirst || {}) as { presenceId: string }; + } else { + params = { + presenceId: paramsOrFirst as string + }; + } + + const presenceId = params.presenceId; + + if (typeof presenceId === 'undefined') { + throw new AppwriteException('Missing required parameter: "presenceId"'); + } + + const apiPath = '/presences/{presenceId}'.replace('{presenceId}', presenceId); + const payload: Payload = {}; + + const uri = new URL(this.client.config.endpoint + apiPath); + return this.client.call('delete', uri, { + 'content-type': 'application/json', + }, payload); + } +}; diff --git a/src/services/realtime.ts b/src/services/realtime.ts index b9e44aca..d85053d8 100644 --- a/src/services/realtime.ts +++ b/src/services/realtime.ts @@ -53,10 +53,31 @@ export type RealtimeResponseConnected = { } export type RealtimeRequest = { - type: 'authentication' | 'subscribe' | 'unsubscribe'; + type: 'authentication' | 'subscribe' | 'unsubscribe' | 'presence'; data: any; } +export type RealtimePresence = { + $id: string; + $sequence?: string | number; + $createdAt: string; + $updatedAt: string; + $permissions: string[]; + userInternalId: string; + userId: string; + status?: string; + source: string; + expiry?: string; + metadata?: Record; +} + +export type RealtimePresenceCreate = { + status: string; + presenceId: string; + permissions?: string[]; + metadata?: Record; +} + type RealtimeRequestSubscribeRow = { subscriptionId?: string; channels: string[]; @@ -74,7 +95,6 @@ export class Realtime { private readonly TYPE_EVENT = 'event'; private readonly TYPE_PONG = 'pong'; private readonly TYPE_CONNECTED = 'connected'; - private readonly TYPE_RESPONSE = 'response'; private readonly DEBOUNCE_MS = 1; private readonly HEARTBEAT_INTERVAL = 20000; // 20 seconds in milliseconds @@ -82,7 +102,13 @@ export class Realtime { private socket?: WebSocket; private activeSubscriptions = new Map>(); private pendingSubscribes = new Map(); + private pendingPresence?: Record; + private appConnected = false; private heartbeatTimer?: number; + // Single-flight lock for createSocket(). When set, concurrent callers join + // this promise instead of issuing a second `new WebSocket(...)`. Cleared + // after the underlying connect resolves or rejects. + private socketCreationPromise?: Promise; private subCallDepth = 0; private reconnectAttempts = 0; @@ -143,8 +169,33 @@ export class Realtime { } } + /** + * Idempotent socket opener. Both `subscribe()` and `upsertPresence()` can + * call this; the single-flight lock (`socketCreationPromise`) guarantees + * only one `new WebSocket(...)` is ever in flight, so concurrent callers + * join the same connection attempt instead of opening duplicates. + * + * Returns early when a healthy socket is already present. + */ private async createSocket(): Promise { - if (this.activeSubscriptions.size === 0) { + // Fast path: a usable socket is already there. No need to open another. + if (this.socket && this.socket.readyState < WebSocket.CLOSING) { + return; + } + // Another caller is already opening one — join it. + if (this.socketCreationPromise) { + return this.socketCreationPromise; + } + this.socketCreationPromise = this.createSocketLocked().finally(() => { + this.socketCreationPromise = undefined; + }); + return this.socketCreationPromise; + } + + private async createSocketLocked(): Promise { + // Nothing to do if there's neither a subscription nor a queued presence + // that needs the wire. (Reconnect cleanup path also flows through here.) + if (this.activeSubscriptions.size === 0 && !this.pendingPresence) { this.reconnect = false; await this.closeSocket(); return; @@ -177,6 +228,15 @@ export class Realtime { } return new Promise((resolve, reject) => { + // Re-check the entry guard synchronously. `disconnect()` may have + // run during the `await this.closeSocket()` above (or any other + // await between the original guard and here), clearing every + // subscription and the pending presence. In that case opening a + // fresh socket would leak a connection with nothing attached. + if (this.activeSubscriptions.size === 0 && !this.pendingPresence) { + resolve(); + return; + } try { const connectionId = ++this.connectionId; const WebSocketCtor: any = WebSocket; @@ -212,6 +272,7 @@ export class Realtime { if (connectionId !== this.connectionId || socket !== this.socket) { return; } + this.appConnected = false; this.stopHeartbeat(); this.onCloseCallbacks.forEach(callback => callback()); @@ -332,7 +393,19 @@ export class Realtime { public async disconnect(): Promise { this.activeSubscriptions.clear(); this.pendingSubscribes.clear(); + this.pendingPresence = undefined; + this.appConnected = false; this.reconnect = false; + // Drop the in-flight single-flight slot. Promises can't be cancelled, + // so the underlying createSocketLocked() promise may stay pending + // forever — e.g. when closeSocket() below tears down a CONNECTING + // socket, the `close` event fires but `open`/`error` never do, and + // the inner `new Promise(...)` only resolves on those. Without this + // line, the next subscribe()/upsertPresence() would join the orphan + // promise via the single-flight gate and hang, leaving its pending + // subscription queued with no socket ever opened. Mirrors the Swift + // template's socketCreationTask cancel in disconnect(). + this.socketCreationPromise = undefined; await this.closeSocket(); } @@ -340,6 +413,18 @@ export class Realtime { if (!this.socket || this.socket.readyState !== WebSocket.OPEN) { return; } + // The WebSocket 'open' event fires when the TCP/upgrade handshake + // completes — but the server only accepts `subscribe` frames after + // it has emitted its own application-level `connected` event (which + // flips `appConnected` to true in handleResponseConnected). Sending + // before then triggers a policy-violation close on real Appwrite, + // which reconnects, which sends early again — i.e. a duplicate- + // socket loop. handleResponseConnected re-enqueues every active + // subscription and calls this method again once it's safe, so the + // queued rows are guaranteed to be sent. + if (!this.appConnected) { + return; + } if (this.pendingSubscribes.size < 1) { return; @@ -538,6 +623,66 @@ export class Realtime { return { unsubscribe, update, close }; } + /** + * Fire-and-forget presence upsert. Records the latest payload in state so + * that — if the WebSocket isn't open yet, or later reconnects — only the + * most recent presence is automatically (re)sent on the next `connected` + * event. Repeated calls while the socket is closed collapse to the latest + * payload (older ones are discarded). + * + * Returns a `Promise` for API consistency; the promise resolves as + * soon as the payload has been stored and the opportunistic send attempted. + * + * @param {RealtimePresenceCreate} params - Presence payload (status and presenceId required, permissions/metadata optional) + */ + public async upsertPresence(params: RealtimePresenceCreate): Promise { + const data: Record = { + status: params.status, + presenceId: params.presenceId, + }; + if (params.permissions !== undefined) { + data.permissions = params.permissions; + } + if (params.metadata !== undefined) { + data.metadata = params.metadata; + } + + this.pendingPresence = data; + + // Both subscribe() and upsertPresence() may need to open the socket. + // createSocket() is single-flight (see `socketCreationPromise`), so + // calling it here is a no-op when a connection is already in flight or + // healthy. Fire-and-forget keeps the documented fire-and-forget shape + // of upsertPresence: the returned Promise resolves as soon as the + // payload is stored. + if (!this.socket || this.socket.readyState >= WebSocket.CLOSING) { + this.createSocket().catch((error) => { + console.error('Failed to open realtime socket for presence:', error); + }); + } + + // Opportunistic send for the case where the socket is already past the + // `connected` handshake. The gate inside flushPendingPresence keeps + // this a no-op until appConnected flips to true. + this.flushPendingPresence(); + } + + private flushPendingPresence(): void { + if (!this.pendingPresence) { + return; + } + if (!this.socket || this.socket.readyState !== WebSocket.OPEN) { + return; + } + if (!this.appConnected) { + return; + } + this.socket.send(JSONbig.stringify({ + type: 'presence', + data: this.pendingPresence + })); + } + private handleMessage(message: RealtimeResponse): void { if (!message.type) { return; @@ -556,9 +701,6 @@ export class Realtime { case this.TYPE_PONG: // Handle pong response if needed break; - case this.TYPE_RESPONSE: - this.handleResponseAction(message); - break; } } @@ -591,7 +733,9 @@ export class Realtime { for (const subscriptionId of this.activeSubscriptions.keys()) { this.enqueuePendingSubscribe(subscriptionId); } + this.appConnected = true; this.sendPendingSubscribes(); + this.flushPendingPresence(); } private handleResponseError(message: RealtimeResponse): void { @@ -632,10 +776,4 @@ export class Realtime { }); } } - - private handleResponseAction(_message: RealtimeResponse): void { - // The SDK generates subscriptionIds client-side and sends them on every - // subscribe/unsubscribe, so subscribe/unsubscribe acks carry no state - // the SDK needs to reconcile. - } } From 7cc9da3d5a0bceb690dc2a33b9e3b40431cdd9d3 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 18 May 2026 13:38:14 +0000 Subject: [PATCH 3/3] chore: update React Native SDK to 0.30.0 --- src/services/realtime.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/services/realtime.ts b/src/services/realtime.ts index d85053d8..3f04d946 100644 --- a/src/services/realtime.ts +++ b/src/services/realtime.ts @@ -67,7 +67,6 @@ export type RealtimePresence = { userId: string; status?: string; source: string; - expiry?: string; metadata?: Record; }