diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index e6ea266e..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@v4
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Use Node.js
- uses: actions/setup-node@v4
+ 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 7492c403..c55ea7bc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,10 +1,19 @@
# Change Log
+## 25.1.0
+
+* 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`
+
## 25.0.0
-* Breaking: Added `unsubscribe()`, `update()`, and `close()` for Realtime subscription lifecycle.
-* Added: Added `userPhone` to the `Membership` model.
-* Updated: Updated `X-Appwrite-Response-Format` header to `1.9.2`.
+* Breaking: Added `unsubscribe()`, `update()`, and `close()` to Realtime subscriptions
+* Added: Added `userPhone` field to `Membership` model
+* Updated: Updated `X-Appwrite-Response-Format` header to `1.9.2`
## 24.2.0
diff --git a/README.md b/README.md
index 6ed4881e..91363506 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
# Appwrite Web SDK

-
+
[](https://travis-ci.com/appwrite/sdk-generator)
[](https://twitter.com/appwrite)
[](https://appwrite.io/discord)
@@ -33,7 +33,7 @@ import { Client, Account } from "appwrite";
To install with a CDN (content delivery network) add the following scripts to the bottom of your
tag, but before you use any Appwrite services:
```html
-
+
```
diff --git a/docs/examples/advisor/get-insight.md b/docs/examples/advisor/get-insight.md
new file mode 100644
index 00000000..28661f98
--- /dev/null
+++ b/docs/examples/advisor/get-insight.md
@@ -0,0 +1,16 @@
+```javascript
+import { Client, Advisor } from "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..79736784
--- /dev/null
+++ b/docs/examples/advisor/get-report.md
@@ -0,0 +1,15 @@
+```javascript
+import { Client, Advisor } from "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..c9b8f2ea
--- /dev/null
+++ b/docs/examples/advisor/list-insights.md
@@ -0,0 +1,17 @@
+```javascript
+import { Client, Advisor } from "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..9480e970
--- /dev/null
+++ b/docs/examples/advisor/list-reports.md
@@ -0,0 +1,16 @@
+```javascript
+import { Client, Advisor } from "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 29241151..71dd8cc3 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..05d740bb
--- /dev/null
+++ b/docs/examples/presences/delete.md
@@ -0,0 +1,15 @@
+```javascript
+import { Client, Presences } from "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..dc4f2438
--- /dev/null
+++ b/docs/examples/presences/get.md
@@ -0,0 +1,15 @@
+```javascript
+import { Client, Presences } from "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..4e7c8762
--- /dev/null
+++ b/docs/examples/presences/list.md
@@ -0,0 +1,17 @@
+```javascript
+import { Client, Presences } from "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..79e5aa91
--- /dev/null
+++ b/docs/examples/presences/update.md
@@ -0,0 +1,20 @@
+```javascript
+import { Client, Presences, Permission, Role } from "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: [Permission.read(Role.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..e8fb6349
--- /dev/null
+++ b/docs/examples/presences/upsert.md
@@ -0,0 +1,19 @@
+```javascript
+import { Client, Presences, Permission, Role } from "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: [Permission.read(Role.any())], // optional
+ expiresAt: '2020-10-15T06:38:00.000+00:00', // optional
+ metadata: {} // optional
+});
+
+console.log(result);
+```
diff --git a/package-lock.json b/package-lock.json
index fc146255..0adf962c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "appwrite",
- "version": "25.0.0",
+ "version": "25.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "appwrite",
- "version": "25.0.0",
+ "version": "25.1.0",
"license": "BSD-3-Clause",
"dependencies": {
"json-bigint": "1.0.0"
diff --git a/package.json b/package.json
index feea2e8e..42b5b992 100644
--- a/package.json
+++ b/package.json
@@ -2,7 +2,7 @@
"name": "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": "25.0.0",
+ "version": "25.1.0",
"license": "BSD-3-Clause",
"main": "dist/cjs/sdk.js",
"exports": {
diff --git a/src/channel.ts b/src/channel.ts
index a5d7ee38..d4618a36 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) {
@@ -82,7 +83,7 @@ export class Channel {
return this.resolve("create");
}
- upsert(this: Channel): Channel {
+ upsert(this: Channel): Channel {
return this.resolve("upsert");
}
@@ -123,6 +124,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";
}
@@ -151,8 +156,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 6bd679d2..67ad9829 100644
--- a/src/client.ts
+++ b/src/client.ts
@@ -358,6 +358,7 @@ class Client {
locale: string;
session: string;
devkey: string;
+ cookie: string;
impersonateuserid: string;
impersonateuseremail: string;
impersonateuserphone: string;
@@ -369,6 +370,7 @@ class Client {
locale: '',
session: '',
devkey: '',
+ cookie: '',
impersonateuserid: '',
impersonateuseremail: '',
impersonateuserphone: '',
@@ -380,8 +382,8 @@ class Client {
'x-sdk-name': 'Web',
'x-sdk-platform': 'client',
'x-sdk-language': 'web',
- 'x-sdk-version': '25.0.0',
- 'X-Appwrite-Response-Format': '1.9.2',
+ 'x-sdk-version': '25.1.0',
+ 'X-Appwrite-Response-Format': '1.9.5',
};
/**
@@ -508,6 +510,20 @@ class Client {
this.config.devkey = value;
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/index.ts b/src/index.ts
index 4041f75f..49067db4 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -13,6 +13,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 a22d5b76..b1ef4d8f 100644
--- a/src/models.ts
+++ b/src/models.ts
@@ -36,6 +36,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
*/
@@ -232,6 +246,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
*/
@@ -310,6 +352,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
*/
@@ -1392,4 +1477,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/account.ts b/src/services/account.ts
index d15c2c71..5a3f7926 100644
--- a/src/services/account.ts
+++ b/src/services/account.ts
@@ -1863,7 +1863,7 @@ export class Account {
* 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.
@@ -1879,7 +1879,7 @@ export class Account {
* 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.
@@ -2616,7 +2616,7 @@ export class Account {
*
* 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.
@@ -2631,7 +2631,7 @@ export class Account {
*
* 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.
diff --git a/src/services/advisor.ts b/src/services/advisor.ts
new file mode 100644
index 00000000..a2365390
--- /dev/null
+++ b/src/services/advisor.ts
@@ -0,0 +1,255 @@
+import { Service } from '../service';
+import { AppwriteException, Client, type Payload, UploadProgress } from '../client';
+import type { Models } from '../models';
+
+
+export class Advisor {
+ client: Client;
+
+ constructor(client: Client) {
+ this.client = 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);
+
+ const apiHeaders: { [header: string]: string } = {
+ }
+
+ return this.client.call(
+ 'get',
+ uri,
+ apiHeaders,
+ 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);
+
+ const apiHeaders: { [header: string]: string } = {
+ }
+
+ return this.client.call(
+ 'get',
+ uri,
+ apiHeaders,
+ 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);
+
+ const apiHeaders: { [header: string]: string } = {
+ }
+
+ return this.client.call(
+ 'get',
+ uri,
+ apiHeaders,
+ 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);
+
+ const apiHeaders: { [header: string]: string } = {
+ }
+
+ return this.client.call(
+ 'get',
+ uri,
+ apiHeaders,
+ payload
+ );
+ }
+}
diff --git a/src/services/presences.ts b/src/services/presences.ts
new file mode 100644
index 00000000..f9df9e18
--- /dev/null
+++ b/src/services/presences.ts
@@ -0,0 +1,363 @@
+import { Service } from '../service';
+import { AppwriteException, Client, type Payload, UploadProgress } from '../client';
+import type { Models } from '../models';
+
+
+export class Presences {
+ client: Client;
+
+ constructor(client: Client) {
+ this.client = 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);
+
+ const apiHeaders: { [header: string]: string } = {
+ }
+
+ return this.client.call(
+ 'get',
+ uri,
+ apiHeaders,
+ 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);
+
+ const apiHeaders: { [header: string]: string } = {
+ }
+
+ return this.client.call(
+ 'get',
+ uri,
+ apiHeaders,
+ 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);
+
+ const apiHeaders: { [header: string]: string } = {
+ 'content-type': 'application/json',
+ }
+
+ return this.client.call(
+ 'put',
+ uri,
+ apiHeaders,
+ 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);
+
+ const apiHeaders: { [header: string]: string } = {
+ 'content-type': 'application/json',
+ }
+
+ return this.client.call(
+ 'patch',
+ uri,
+ apiHeaders,
+ 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);
+
+ const apiHeaders: { [header: string]: string } = {
+ 'content-type': 'application/json',
+ }
+
+ return this.client.call(
+ 'delete',
+ uri,
+ apiHeaders,
+ payload
+ );
+ }
+}
diff --git a/src/services/realtime.ts b/src/services/realtime.ts
index a1368b9e..7aebdaa6 100644
--- a/src/services/realtime.ts
+++ b/src/services/realtime.ts
@@ -52,10 +52,30 @@ 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;
+ metadata?: Record;
+}
+
+export type RealtimePresenceCreate = {
+ status: string;
+ presenceId: string;
+ permissions?: string[];
+ metadata?: Record;
+}
+
type RealtimeRequestSubscribeRow = {
subscriptionId?: string;
channels: string[];
@@ -73,7 +93,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
@@ -81,7 +100,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;
@@ -142,8 +167,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;
@@ -176,6 +226,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 socket = (this.socket = new WebSocket(url));
@@ -206,6 +265,7 @@ export class Realtime {
if (connectionId !== this.connectionId || socket !== this.socket) {
return;
}
+ this.appConnected = false;
this.stopHeartbeat();
this.onCloseCallbacks.forEach(callback => callback());
@@ -326,7 +386,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();
}
@@ -334,6 +406,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;
@@ -532,6 +616,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;
@@ -550,9 +694,6 @@ export class Realtime {
case this.TYPE_PONG:
// Handle pong response if needed
break;
- case this.TYPE_RESPONSE:
- this.handleResponseAction(message);
- break;
}
}
@@ -585,7 +726,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 {
@@ -626,10 +769,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.
- }
}