diff --git a/CHANGELOG.md b/CHANGELOG.md index 7900433b..df35480f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Change log +## 1.2.0-rc.0 + +* Added: `Apps` service to manage OAuth2 apps, keys, secrets, and installations +* Added: `Oauth2` service for authorization, consent, device, PAR, and token flows +* Added: `Push` service for MQTT realtime push with Android background delivery +* Added: `Topic` builder and native TCP modules for push +* Added: `Avatars.updatePhoto()` and `Avatars.deletePhoto()` methods +* Added: `App`, `Oauth2*`, and `Account` models +* Added: `Webflow` to the `OAuthProvider` enum +* Fixed: cookie session fallback is guarded to Expo web where `localStorage` exists + ## 1.1.0 * Added: `Account.createRecoveryOTP` and `Account.updateRecoveryOTP` for code-based password recovery diff --git a/README.md b/README.md index a9182ffe..b4ee0eda 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,54 @@ To install npx expo install react-native-appwrite react-native-url-polyfill ``` +### Push + +`Push` connects over a raw TCP socket, so it needs `react-native-tcp-socket` and a development +or standalone build (native modules do not run in Expo Go). + +On Expo SDK 52 and older, enable package exports in `metro.config.js` so `mqtt` resolves to its +React Native build (Expo SDK 53 and later do this by default): + +```js +config.resolver.unstable_enablePackageExports = true; +``` + +#### Background delivery on Android + +A subscription with `background: true` keeps delivering after the app is backgrounded, killed or +the device restarts, until it is unsubscribed or `push.close()` is called (do this on sign-out). +The SDK's native Android module (autolinked) saves the subscription, and a scheduled job and +alarm wake the app every 15 to 60 seconds to reconnect; the broker replays what was sent in +between (`retry: true`). Messages no in-app callback receives are posted as notifications that +open the app. It reconnects with the credential saved at subscribe time, so use a session rather +than a short-lived JWT. + +On Android 13 and later, notifications only appear once the user grants the +`POST_NOTIFICATIONS` runtime permission (the SDK declares it in its manifest). Without it, the +subscription still delivers to your callback but posts no notification, so ask before subscribing: + +```js +import { PermissionsAndroid, Platform } from 'react-native'; + +if (Platform.OS === 'android' && Platform.Version >= 33) { + await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS); +} + +const sub = await push.subscribe('news', (message) => console.log(message.data), { + background: true, + title: 'News', +}); + +// Optional: immediate delivery even after a kill and during Doze, with a quiet ongoing +// notification (call while the app is in the foreground). +await push.setForeground(true); +``` + +Foreground mode runs a `remoteMessaging` foreground service, which Google Play asks apps to +declare in the Play Console. Apps that never enable it can remove the service from their merged +manifest with `tools:node="remove"` on `io.appwrite.services.PushService` and +`android.permission.FOREGROUND_SERVICE_REMOTE_MESSAGING`. + ## Getting Started diff --git a/android/.editorconfig b/android/.editorconfig new file mode 100644 index 00000000..f0895a33 --- /dev/null +++ b/android/.editorconfig @@ -0,0 +1,39 @@ +root = true + +[*.{kt,kts}] +ktlint_code_style = ktlint_official +ktlint_standard_annotation = disabled +ktlint_standard_argument-list-wrapping = disabled +ktlint_standard_blank-line-before-declaration = disabled +ktlint_standard_blank-line-between-when-conditions = disabled +ktlint_standard_chain-method-continuation = disabled +ktlint_standard_class-signature = disabled +ktlint_standard_function-expression-body = disabled +ktlint_standard_function-signature = disabled +ktlint_standard_function-start-of-body-spacing = disabled +ktlint_standard_if-else-bracing = disabled +ktlint_standard_if-else-wrapping = disabled +ktlint_standard_max-line-length = disabled +ktlint_standard_multiline-expression-wrapping = disabled +ktlint_standard_multiline-if-else = disabled +ktlint_standard_no-blank-line-before-rbrace = disabled +ktlint_standard_no-blank-line-in-list = disabled +ktlint_standard_no-empty-first-line-in-class-body = disabled +ktlint_standard_no-line-break-before-assignment = disabled +ktlint_standard_no-wildcard-imports = disabled +ktlint_standard_parameter-list-wrapping = disabled +ktlint_standard_spacing-between-declarations-with-annotations = disabled +ktlint_standard_spacing-between-declarations-with-comments = disabled +ktlint_standard_string-template = disabled +ktlint_standard_trailing-comma-on-call-site = disabled +ktlint_standard_trailing-comma-on-declaration-site = disabled +ktlint_standard_when-entry-bracing = disabled +ktlint_standard_wrapping = disabled + +[**/Channel.kt] +# Marker types intentionally use leading underscores to distinguish channel states. +ktlint_standard_class-naming = disabled + +[**/models/*.kt] +# Model imports follow schema property order so generated declarations stay deterministic. +ktlint_standard_import-ordering = disabled diff --git a/android/build.gradle b/android/build.gradle new file mode 100644 index 00000000..024c3a52 --- /dev/null +++ b/android/build.gradle @@ -0,0 +1,40 @@ +// The native half of Push on Android: background delivery that survives the process being +// killed, a reboot and an app update, shared with the Appwrite Android SDK. +def safeExtGet(prop, fallback) { + rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback +} + +apply plugin: "com.android.library" +apply plugin: "kotlin-android" + +android { + namespace "io.appwrite.reactnative" + compileSdkVersion safeExtGet("compileSdkVersion", 35) + + defaultConfig { + minSdkVersion safeExtGet("minSdkVersion", 24) + targetSdkVersion safeExtGet("targetSdkVersion", 35) + // R8 rules for the bundled MQTT client, applied to the app. + consumerProguardFiles "consumer-rules.pro" + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = "17" + } +} + +repositories { + google() + mavenCentral() +} + +dependencies { + implementation "com.facebook.react:react-android" + implementation "com.hivemq:hivemq-mqtt-client-shaded:1.3.6" + implementation "androidx.core:core-ktx:1.13.1" +} diff --git a/android/consumer-rules.pro b/android/consumer-rules.pro new file mode 100644 index 00000000..04d7db2c --- /dev/null +++ b/android/consumer-rules.pro @@ -0,0 +1,27 @@ +# Push: rules applied to apps that shrink with R8. +# +# The shaded HiveMQ MQTT client relocates Netty and JCTools under +# com.hivemq.client.internal.shaded. Netty references optional classes that are never loaded +# on Android: native transports, the HTTP/WebSocket codecs, proxy handlers, alternative TLS +# providers (tcnative, BouncyCastle, Conscrypt, Jetty ALPN) and logging backends. +-dontwarn com.hivemq.client.internal.shaded.io.netty.channel.epoll.** +-dontwarn com.hivemq.client.internal.shaded.io.netty.channel.kqueue.** +-dontwarn com.hivemq.client.internal.shaded.io.netty.handler.codec.http.** +-dontwarn com.hivemq.client.internal.shaded.io.netty.handler.proxy.** +-dontwarn com.hivemq.client.internal.shaded.io.netty.internal.tcnative.** +-dontwarn org.bouncycastle.** +-dontwarn org.conscrypt.** +-dontwarn org.eclipse.jetty.** +-dontwarn org.apache.log4j.** +-dontwarn org.apache.logging.log4j.** +-dontwarn org.slf4j.** +-dontwarn reactor.blockhound.** + +# Netty and JCTools reach their own fields by name, through atomic field updaters and Unsafe offsets. +-keepclassmembernames class com.hivemq.client.internal.shaded.io.netty.** { ; } +-keepclassmembers class com.hivemq.client.internal.shaded.org.jctools.** { ; } + +# Netty's leak detector looks these methods up by name when its classes load. +-keepclassmembers class com.hivemq.client.internal.shaded.io.netty.buffer.AbstractByteBufAllocator { *** toLeakAwareBuffer(...); } +-keepclassmembers class com.hivemq.client.internal.shaded.io.netty.buffer.AdvancedLeakAwareByteBuf { *** recordLeakNonRefCountingOperation(...); } +-keepclassmembers class com.hivemq.client.internal.shaded.io.netty.util.ReferenceCountUtil { *** touch(...); } diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml new file mode 100644 index 00000000..44f894ed --- /dev/null +++ b/android/src/main/AndroidManifest.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/src/main/java/io/appwrite/exceptions/AppwriteException.kt b/android/src/main/java/io/appwrite/exceptions/AppwriteException.kt new file mode 100644 index 00000000..81b0d30e --- /dev/null +++ b/android/src/main/java/io/appwrite/exceptions/AppwriteException.kt @@ -0,0 +1,6 @@ +package io.appwrite.exceptions + +/** A push error. When the broker explained a refusal or a disconnect, that is its message. */ +class AppwriteException( + override val message: String? = null, +) : Exception(message) diff --git a/android/src/main/java/io/appwrite/reactnative/AppwriteCookiesModule.kt b/android/src/main/java/io/appwrite/reactnative/AppwriteCookiesModule.kt new file mode 100644 index 00000000..8e1dce18 --- /dev/null +++ b/android/src/main/java/io/appwrite/reactnative/AppwriteCookiesModule.kt @@ -0,0 +1,31 @@ +package io.appwrite.reactnative + +import android.webkit.CookieManager +import com.facebook.react.bridge.Promise +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReactContextBaseJavaModule +import com.facebook.react.bridge.ReactMethod + +class AppwriteCookiesModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) { + override fun getName(): String = NAME + + @ReactMethod + fun session(url: String, project: String, promise: Promise) { + val name = "a_session_$project" + try { + val value = CookieManager.getInstance().getCookie(url) + ?.split(";") + ?.map { it.trim() } + ?.firstOrNull { it.substringBefore("=") == name } + ?.substringAfter("=") + promise.resolve(value) + } catch (e: Exception) { + promise.reject(ERROR_CODE, e.message, e) + } + } + + companion object { + const val NAME = "AppwriteCookies" + private const val ERROR_CODE = "appwrite_cookies" + } +} diff --git a/android/src/main/java/io/appwrite/reactnative/AppwritePushModule.kt b/android/src/main/java/io/appwrite/reactnative/AppwritePushModule.kt new file mode 100644 index 00000000..454cc8db --- /dev/null +++ b/android/src/main/java/io/appwrite/reactnative/AppwritePushModule.kt @@ -0,0 +1,127 @@ +package io.appwrite.reactnative + +import android.util.Base64 +import com.facebook.react.bridge.Arguments +import com.facebook.react.bridge.Promise +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReactContextBaseJavaModule +import com.facebook.react.bridge.ReactMethod +import com.facebook.react.modules.core.DeviceEventManagerModule +import io.appwrite.services.PushBridge +import io.appwrite.services.PushMessage + +/** + * The React Native side of [PushBridge]: the SDK's `Push` hosts its background subscriptions + * here on Android, and receives their messages and errors as events. + * + * [emit] sends an event to JS; tests replace it to observe what JS would receive. + */ +class AppwritePushModule internal constructor( + private val reactContext: ReactApplicationContext, + private val emit: (String, Map) -> Unit, +) : ReactContextBaseJavaModule(reactContext) { + constructor(reactContext: ReactApplicationContext) : this( + reactContext, + { event, body -> + if (reactContext.hasActiveReactInstance()) { + reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) + .emit(event, Arguments.makeNativeMap(body)) + } + }, + ) + + private val bridge = PushBridge( + reactContext, + object : PushBridge.Events { + override fun onMessage(subscriptionId: String, message: PushMessage, ackToken: String) = emit( + MESSAGE_EVENT, + mapOf( + "id" to subscriptionId, + "topic" to message.topic, + "payload" to Base64.encodeToString(message.payload, Base64.NO_WRAP), + "qos" to message.qos, + "ackToken" to ackToken, + ), + ) + + override fun onError(message: String) = emit(ERROR_EVENT, mapOf("message" to message)) + }, + ) + + override fun getName(): String = NAME + + // Resolves once the connection is up and every filter is subscribed, or rejects with why not. + @ReactMethod + fun host(config: String, subscriptions: String, promise: Promise) { + try { + bridge.host(config, subscriptions) { error -> + if (error == null) { + promise.resolve(null) + } else { + promise.reject(ERROR_CODE, error) + } + } + } catch (e: Exception) { + promise.reject(ERROR_CODE, e.message, e) + } + } + + @ReactMethod + fun ack(token: String) = bridge.ack(token) + + @ReactMethod + fun release(promise: Promise) = settle(promise) { + bridge.release() + null + } + + @ReactMethod + fun stop(promise: Promise) = settle(promise) { + bridge.stop() + null + } + + @ReactMethod + fun setForeground(enabled: Boolean, promise: Promise) = settle(promise) { + bridge.setForeground(enabled) + null + } + + @ReactMethod + fun hasSaved(promise: Promise) = settle(promise) { bridge.hasSaved() } + + @ReactMethod + fun resume(promise: Promise) = settle(promise) { + bridge.resume() + null + } + + @ReactMethod + fun setErrorCallback(registered: Boolean, promise: Promise) = settle(promise) { bridge.setErrorCallback(registered) } + + @ReactMethod + fun defaultClientId(authMethod: String, credential: String, promise: Promise) = + settle(promise) { bridge.defaultClientId(authMethod, credential) } + + // Required by NativeEventEmitter; events are emitted whether or not anyone listens. + @ReactMethod + fun addListener(eventName: String) = Unit + + @ReactMethod + fun removeListeners(count: Double) = Unit + + private fun settle(promise: Promise, block: () -> Any?) { + try { + promise.resolve(block()) + } catch (e: Exception) { + promise.reject(ERROR_CODE, e.message, e) + } + } + + companion object { + const val NAME = "AppwritePush" + const val MESSAGE_EVENT = "AppwritePushMessage" + const val ERROR_EVENT = "AppwritePushError" + private const val ERROR_CODE = "appwrite_push" + } +} diff --git a/android/src/main/java/io/appwrite/reactnative/AppwritePushPackage.kt b/android/src/main/java/io/appwrite/reactnative/AppwritePushPackage.kt new file mode 100644 index 00000000..728186af --- /dev/null +++ b/android/src/main/java/io/appwrite/reactnative/AppwritePushPackage.kt @@ -0,0 +1,17 @@ +package io.appwrite.reactnative + +import com.facebook.react.ReactPackage +import com.facebook.react.bridge.NativeModule +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.uimanager.ViewManager + +/** Registers [AppwritePushModule] and [AppwriteCookiesModule]; found by React Native autolinking. */ +class AppwritePushPackage : ReactPackage { + override fun createNativeModules(reactContext: ReactApplicationContext): List = + listOf( + AppwritePushModule(reactContext), + AppwriteCookiesModule(reactContext), + ) + + override fun createViewManagers(reactContext: ReactApplicationContext): List> = emptyList() +} diff --git a/android/src/main/java/io/appwrite/services/PushBackground.kt b/android/src/main/java/io/appwrite/services/PushBackground.kt new file mode 100644 index 00000000..52517344 --- /dev/null +++ b/android/src/main/java/io/appwrite/services/PushBackground.kt @@ -0,0 +1,1009 @@ +package io.appwrite.services + +import android.app.AlarmManager +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.app.job.JobInfo +import android.app.job.JobScheduler +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.net.ConnectivityManager +import android.net.Network +import android.os.Build +import android.os.PowerManager +import android.os.SystemClock +import android.util.AtomicFile +import android.util.Log +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import androidx.core.content.ContextCompat +import com.hivemq.client.mqtt.MqttGlobalPublishFilter +import com.hivemq.client.mqtt.datatypes.MqttQos +import com.hivemq.client.mqtt.mqtt5.Mqtt5AsyncClient +import com.hivemq.client.mqtt.mqtt5.exceptions.Mqtt5ConnAckException +import com.hivemq.client.mqtt.mqtt5.message.publish.Mqtt5Publish +import io.appwrite.exceptions.AppwriteException +import org.json.JSONArray +import org.json.JSONObject +import java.io.File +import java.io.IOException +import java.util.UUID +import java.util.concurrent.CopyOnWriteArrayList +import java.util.concurrent.ExecutionException +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit +import java.util.concurrent.TimeoutException +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicInteger + +private const val LOG_TAG = "AppwritePush" + +/** A background subscription saved across restarts: its filter, QoS choice and notification title. */ +internal data class PushEntry( + val filter: String, + val retry: Boolean, + val title: String?, +) + +/** + * The adaptive heartbeat: its interval, the current streak of failed heartbeats, and when the + * interval last shrank (wall-clock ms, as it must survive reboots). + */ +internal data class PushHeartbeat( + val intervalMs: Long, + val failures: Int = 0, + val shrunkAt: Long = 0L, +) + +/** + * The process-wide host of the background push connection, modelled on how socket push + * services survive on Android without keeping a service alive. + * + * The background subscriptions and their connection config are saved to private storage (see + * [PushStore]), so delivery survives the process being killed, a reboot and an app update. A + * job ([PushJobService]) that reschedules itself on every run, backed by an alarm + * ([PushAlarmReceiver]), wakes the process every 15 seconds (60 when the app may use exact + * alarms or is exempt from battery optimisation). Each run holds a short wakelock and + * reconnects when the connection is gone or the default network changed, sends a heartbeat + * when one is due, and re-arms the job. While the process is alive between runs the socket + * stays open and messages arrive immediately; while it is dead the broker keeps the backlog + * for this client id and replays it when the next run subscribes again. + * + * With foreground mode enabled ([Push.setForeground]) a foreground service ([PushService]) also keeps + * the process at foreground priority, so delivery is immediate after a kill and during Doze. + * + * Messages go to the live in-app callbacks that match them. When none match (the process was + * started in the background), a declared [PushReceiver] gets the message, and a notification + * is posted unless it returns true. + */ +internal object PushBackground { + const val CHANNEL_ID = "appwrite-push" + const val NOTIFICATION_ID = 1911 + const val ACTION_TICK = "io.appwrite.push.TICK" + const val ACTION_MESSAGE = "io.appwrite.push.MESSAGE" + const val NOTIFICATION_ICON = "io.appwrite.push.notification_icon" + const val EXTRA_TOPIC = "io.appwrite.push.TOPIC" + const val EXTRA_PAYLOAD = "io.appwrite.push.PAYLOAD" + + private const val SERVICE_CHANNEL_ID = "appwrite-push-service" + private const val JOB_ID = 0x50555348 + private const val INTERVAL_MS = 15_000L + private const val PRIVILEGED_INTERVAL_MS = 60_000L + private const val RESTART_DELAY_MS = 3_000L + // An upper bound covering a run's connect (20 s) and its requests (10 s each); the wakelock is + // released as soon as the run finishes. + private const val WAKE_LOCK_MS = 60_000L + private const val KEEP_ALIVE_SECONDS = 300 + + // The heartbeat interval: below the broker's 1.5 x keep-alive expiry, measured on a clock that + // counts deep sleep. Networks that drop idle connections sooner (carrier NATs) make heartbeats + // fail; after HEARTBEAT_FAILURES in a row the interval shrinks by a quarter, down to + // MIN_HEARTBEAT_MS, and returns to the default HEARTBEAT_RESET_MS after the last shrink, in + // case the device moved to a better network. + const val HEARTBEAT_MS = 180_000L + private const val MIN_HEARTBEAT_MS = 60_000L + private const val HEARTBEAT_FAILURES = 3 + private const val HEARTBEAT_RESET_MS = 3 * 24 * 60 * 60 * 1_000L + private const val REQUEST_TIMEOUT_SECONDS = 10L + private const val CONNECT_TIMEOUT_SECONDS = 20L + + // How long a message waits for a listener that acknowledges it itself (the React Native and + // Flutter bridges, once their callback has run) before it is acknowledged anyway, so one stuck + // callback cannot hold back the acknowledgements after it. + private const val ACK_TIMEOUT_SECONDS = 10L + + // Unsubscribing a filter nothing subscribes to is a harmless round trip the broker always + // acknowledges, so it proves the connection is alive. The broker ignores client PUBLISHes + // without a PUBACK, so a publish cannot serve as the heartbeat. + private const val HEARTBEAT_FILTER = "\$heartbeat" + + @Volatile + var channelName: String = "Notifications" + + @Volatile + var ongoingTitle: String = "Appwrite" + + // Reports errors to the Push instance that hosts its subscriptions here. Returns whether the + // app's onError received the error (false when it registered none). + @Volatile + var errorHandler: ((Throwable) -> Boolean)? = null + + @Volatile + var serviceRunning = false + + // Whether a refused service start was reported, so a job that retries it every run does not + // report the same failure every 15 seconds. + @Volatile + private var serviceStartReported = false + + // The live in-app subscriptions hosted here, each with its callback. Empty after the process + // was restarted in the background, until the app subscribes again. + val listeners = CopyOnWriteArrayList() + + // Guards the saved state below and its copy on disk. + private val lock = Any() + private var loaded = false + private var config: PushConfig? = null + + // Background subscriptions saved by an earlier run that no live subscription has taken over. + private var saved: List = emptyList() + + // Every MQTT call runs on this thread, in order, so the connection state below needs no lock. + private val worker = Executors.newSingleThreadScheduledExecutor { runnable -> + Thread(runnable, "AppwritePushBackground").apply { isDaemon = true } + } + private var mqtt: Mqtt5AsyncClient? = null + private var connectedConfig: PushConfig? = null + private var connectedNetwork: Network? = null + private var lastHeartbeat = 0L + private var heartbeatState: PushHeartbeat? = null + private val subscribed = mutableMapOf() + private var networkCallback: ConnectivityManager.NetworkCallback? = null + + // Worker thread only. While a caller waits for a converge (host with a completion), its + // failures go to that caller, which reports them, instead of onError. + private var waiting = false + private var failure: Throwable? = null + + /** + * Host [newListeners] (the app's live subscriptions) on the background connection with + * [newConfig], save the background ones, and arm the scheduled wake-ups. Saved subscriptions + * from an earlier run stay unless a live subscription on the same filter takes them over + * (with background on or off), or [newConfig] is for another client id (another user), whose + * saved subscriptions are dropped. + * + * With [done], it is called once the connection is up and every filter is subscribed (SUBACK), + * or with the failure, which is then not reported to onError: the caller reports it. + */ + fun host( + context: Context, + newConfig: PushConfig, + newListeners: List, + done: ((Throwable?) -> Unit)? = null, + ) { + val app = context.applicationContext + synchronized(lock) { + load(app) + if (config != null && config?.clientId != newConfig.clientId) { + saved = emptyList() + } + // A live subscription on a saved filter takes it over, whether or not it wants background: + // one that turned background off, or is later unsubscribed, must not leave the saved + // entry delivering in the background. + saved = saved.filter { entry -> newListeners.none { entry.filter in it.topics } } + config = newConfig + listeners.clear() + listeners.addAll(newListeners) + persist(app) + } + activate(app, done) + } + + /** + * Stop hosting the app's live subscriptions, keeping the saved ones from an earlier run. + * Shuts down when none remain. + */ + fun release(context: Context) { + val app = context.applicationContext + val active = synchronized(lock) { + listeners.clear() + persist(app) + isActive() + } + if (active) { + worker.execute { converge(app, heartbeat = false) } + } else { + shutdown(app) + } + } + + /** Stop background delivery completely and forget every saved subscription. */ + fun stop(context: Context) { + val app = context.applicationContext + synchronized(lock) { + listeners.clear() + config = null + saved = emptyList() + loaded = true + PushStore.clearState(app) + } + shutdown(app) + } + + /** Whether subscriptions saved by an earlier run are still delivered in the background. */ + fun hasSaved(context: Context): Boolean = synchronized(lock) { + load(context.applicationContext) + saved.isNotEmpty() + } + + /** Persist the foreground-mode choice and start or stop the foreground service to match. */ + fun setForeground(context: Context, enabled: Boolean) { + val app = context.applicationContext + PushStore.setForeground(app, enabled) + serviceStartReported = false + ensureService(app) + } + + /** Whether the foreground service should run: foreground mode is enabled and there is work to do. */ + fun wantsService(context: Context): Boolean { + val app = context.applicationContext + return PushStore.foreground(app) && synchronized(lock) { + load(app) + isActive() + } + } + + /** Resume saved background delivery when the app starts, without waiting for the next run. */ + fun resume(context: Context) { + val app = context.applicationContext + val active = synchronized(lock) { + load(app) + isActive() + } + if (active) { + tick(app) {} + } + } + + /** + * One scheduled run: under a wakelock, reconnect if needed, send a heartbeat when one is due, + * re-arm the next run, and call [done] once finished. Shuts down when nothing is saved. + */ + fun tick(context: Context, done: () -> Unit) { + val app = context.applicationContext + val wakeLock = acquireWakeLock(app) + worker.execute { + try { + val active = synchronized(lock) { + load(app) + isActive() + } + if (active) { + // Arm the next run first, so a run cut short (killed, or a slow broker outlasting + // the wakelock) still leaves the chain armed. A refused credential in converge() + // shuts down, cancelling it again. + schedule(app, INTERVAL_MS) + ensureService(app) + registerNetworkCallback(app) + converge(app, heartbeat = true) + } else { + shutdown(app) + } + } catch (e: Exception) { + report(e) + } finally { + releaseWakeLock(wakeLock) + done() + } + } + } + + /** Run once more shortly after the task was removed, so an OEM kill that follows is undone. */ + fun scheduleRestart(context: Context) { + schedule(context.applicationContext, RESTART_DELAY_MS, jobToo = false) + } + + /** + * Forget everything held in memory, as the process dying does, keeping only what is saved. + * For tests of what happens after the app's process was killed. + */ + internal fun dropProcessState(context: Context) { + synchronized(lock) { + listeners.clear() + config = null + saved = emptyList() + loaded = false + } + errorHandler = null + unregisterNetworkCallback(context.applicationContext) + worker.submit { disconnect() }.get() + } + + /** Report an error to the hosting Push instance's onError; logged when there is none. */ + fun report(error: Throwable): Boolean { + val delivered = try { + errorHandler?.invoke(error) ?: false + } catch (callbackError: Exception) { + Log.e(LOG_TAG, "Push onError threw", callbackError) + true + } + if (!delivered) { + Log.e(LOG_TAG, "Push background error", error) + } + return delivered + } + + // Must hold [lock]. + private fun load(context: Context) { + if (loaded) { + return + } + loaded = true + val state = PushStore.loadState(context) ?: return + config = state.first + saved = state.second + } + + // Must hold [lock]. + private fun isActive(): Boolean = config != null && (saved.isNotEmpty() || listeners.isNotEmpty()) + + // Must hold [lock]. Every background subscription, live or saved, one per filter and title. + // Subscriptions sharing a filter and title are saved once, with QoS 1 when any of them wants it. + private fun entries(): List = + (saved + listeners.filter { it.background }.flatMap { listener -> listener.topics.map { PushEntry(it, listener.retry, listener.title) } }) + .groupBy { it.filter to it.title } + .map { (key, group) -> PushEntry(key.first, group.any { it.retry }, key.second) } + + // Must hold [lock]. + private fun persist(context: Context) { + val current = config + val entries = entries() + if (current == null || entries.isEmpty()) { + PushStore.clearState(context) + } else { + PushStore.saveState(context, current, entries) + } + } + + // Must hold [lock]. The broker subscriptions wanted, at the highest QoS any subscription on a + // filter asks for. + private fun wanted(): Map { + val qos = linkedMapOf() + fun want(filter: String, retry: Boolean) { + if (retry) { + qos[filter] = MqttQos.AT_LEAST_ONCE + } else if (filter !in qos) { + qos[filter] = MqttQos.AT_MOST_ONCE + } + } + for (listener in listeners) { + listener.topics.forEach { want(it, listener.retry) } + } + saved.forEach { want(it.filter, it.retry) } + return qos + } + + private fun activate(context: Context, done: ((Throwable?) -> Unit)? = null) { + registerNetworkCallback(context) + schedule(context, INTERVAL_MS) + ensureService(context) + worker.execute { converge(context, heartbeat = false, done) } + } + + private fun shutdown(context: Context) { + cancelSchedule(context) + unregisterNetworkCallback(context) + if (serviceRunning) { + context.stopService(Intent(context, PushService::class.java)) + } + worker.execute { disconnect() } + } + + // Worker thread only. Bring the connection to the wanted state: reconnect when the config or + // the default network changed or a due heartbeat fails, then subscribe and unsubscribe the + // difference. With [done], failures go to it (see [waiting]) instead of onError. + private fun converge(context: Context, heartbeat: Boolean, done: ((Throwable?) -> Unit)? = null) { + waiting = done != null + failure = null + try { + convergeOnce(context, heartbeat) + } catch (e: Exception) { + fail(e) + } finally { + waiting = false + done?.invoke(failure) + } + } + + // Worker thread only. Record a failure for the waiting caller, or report it. + private fun fail(error: Throwable) { + if (failure == null) { + failure = error + } + if (!waiting) { + report(error) + } + } + + private fun convergeOnce(context: Context, heartbeat: Boolean) { + val (current, wanted) = synchronized(lock) { config to wanted() } + if (current == null || wanted.isEmpty()) { + disconnect() + return + } + var client = mqtt + if (client != null && (current != connectedConfig || networkChanged(context))) { + disconnect() + client = null + } + if (client != null && heartbeat && SystemClock.elapsedRealtime() - lastHeartbeat >= heartbeatInterval(context)) { + val alive = sendHeartbeat(client) + recordHeartbeat(context, alive) + if (alive) { + lastHeartbeat = SystemClock.elapsedRealtime() + } else { + disconnect() + client = null + } + } + if (client == null) { + client = connect(context, current) ?: return + } + for (filter in subscribed.keys - wanted.keys) { + request { client.unsubscribeWith().topicFilter(filter).send().get(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS) } + subscribed.remove(filter) + } + for ((filter, qos) in wanted) { + if (subscribed[filter] != qos && subscribe(client, filter, qos)) { + subscribed[filter] = qos + } + } + } + + // Worker thread only. Connect with [config], or report why not and return null. A refused + // credential stops background delivery: retrying it would only be refused again. + private fun connect(context: Context, config: PushConfig): Mqtt5AsyncClient? { + // A failed first connect of a waited-for converge goes to the waiting caller only. + val quiet = waiting + val connected = AtomicBoolean(false) + val client = buildPushClient( + config.copy(keepAlive = KEEP_ALIVE_SECONDS), + onError = { error -> + if (!quiet || connected.get()) { + report(error) + } + }, + onAuthRefused = { error -> worker.execute { refused(context, error) } }, + ) + // Registered before connecting, so the backlog the broker replays right after each + // SUBSCRIBE reaches the app however the subscription was made (including the ones the + // client restores itself after an automatic reconnect). Acknowledged by hand, once the + // message was delivered (see [deliver]). + client.publishes(MqttGlobalPublishFilter.ALL, { publish -> deliver(context, publish) }, true) + try { + client.sendPushConnect(config.copy(keepAlive = KEEP_ALIVE_SECONDS), CONNECT_TIMEOUT_SECONDS) + } catch (e: Exception) { + val cause = (e as? ExecutionException)?.cause ?: e + client.disconnect() + val error = connectError(cause) + when { + isAuthRefusal(cause) -> refused(context, error) + waiting -> fail(error) + // A network failure already reached onError through the client's disconnect + // listener; a refused CONNECT and a timeout did not. + cause is Mqtt5ConnAckException || cause is TimeoutException -> report(error) + } + return null + } + connected.set(true) + mqtt = client + connectedConfig = config + connectedNetwork = activeNetwork(context) + lastHeartbeat = SystemClock.elapsedRealtime() + subscribed.clear() + return client + } + + // Worker thread only. + private fun subscribe(client: Mqtt5AsyncClient, filter: String, qos: MqttQos): Boolean = request { + val subAck = client.subscribeWith().topicFilter(filter).qos(qos).send().get(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS) + val code = subAck.reasonCodes.firstOrNull { it.isError } + if (code != null) { + throw AppwriteException( + subAck.reasonString.map { it.toString() }.orElse("Subscribing to $filter was refused (reason $code)"), + ) + } + } + + // Worker thread only. The current heartbeat interval, back at the default once a shrink is + // old enough. + private fun heartbeatInterval(context: Context): Long { + val state = heartbeatState ?: PushStore.heartbeat(context).also { heartbeatState = it } + if (state.intervalMs < HEARTBEAT_MS && System.currentTimeMillis() - state.shrunkAt >= HEARTBEAT_RESET_MS) { + saveHeartbeat(context, PushHeartbeat(HEARTBEAT_MS)) + return HEARTBEAT_MS + } + return state.intervalMs + } + + // Worker thread only. Count a failed heartbeat (a connection the network dropped silently), + // shrinking the interval after HEARTBEAT_FAILURES in a row; a delivered one ends the streak. + private fun recordHeartbeat(context: Context, alive: Boolean) { + val state = heartbeatState ?: PushStore.heartbeat(context) + val next = when { + alive -> state.copy(failures = 0) + state.failures + 1 < HEARTBEAT_FAILURES -> state.copy(failures = state.failures + 1) + else -> PushHeartbeat(maxOf(MIN_HEARTBEAT_MS, state.intervalMs * 3 / 4), 0, System.currentTimeMillis()) + } + if (next != state) { + saveHeartbeat(context, next) + } + } + + private fun saveHeartbeat(context: Context, state: PushHeartbeat) { + heartbeatState = state + PushStore.setHeartbeat(context, state) + } + + // Worker thread only. + private fun sendHeartbeat(client: Mqtt5AsyncClient): Boolean = try { + client.unsubscribeWith().topicFilter(HEARTBEAT_FILTER).send().get(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS) + true + } catch (e: Exception) { + false + } + + // Run a broker request, recording or reporting its failure. Returns whether it succeeded. + private fun request(block: () -> Unit): Boolean = try { + block() + true + } catch (e: Exception) { + fail((e as? ExecutionException)?.cause ?: e) + false + } + + // Worker thread only. + private fun disconnect() { + mqtt?.disconnect() + mqtt = null + connectedConfig = null + connectedNetwork = null + subscribed.clear() + } + + // Worker thread only. The broker refused the credential: report it and stop. With no onError + // registered the message is also saved, and the next onError registration receives it. A + // waiting caller receives it instead. + private fun refused(context: Context, error: Throwable) { + if (waiting) { + fail(error) + } else if (!report(error)) { + PushStore.setStoppedError(context, error.message ?: "Push connection refused") + } + stop(context) + } + + // Deliver a message, then acknowledge it: right away, or once every listener that acknowledges + // messages itself has done so (at most ACK_TIMEOUT_SECONDS later). + private fun deliver(context: Context, publish: Mqtt5Publish) { + val acknowledged = AtomicBoolean(false) + val acknowledge = { + if (acknowledged.compareAndSet(false, true)) { + runCatching { publish.acknowledge() } + } + } + val pending = AtomicInteger(1) + val settle = { + if (pending.decrementAndGet() == 0) { + acknowledge() + } + } + try { + deliverMessage(context, publish.toPushMessage()) { + pending.incrementAndGet() + val once = AtomicBoolean(false) + val ack: () -> Unit = { + if (once.compareAndSet(false, true)) { + settle() + } + } + ack + } + } finally { + settle() + } + if (!acknowledged.get()) { + worker.schedule({ acknowledge() }, ACK_TIMEOUT_SECONDS, TimeUnit.SECONDS) + } + } + + // Deliver a message to the matching live callbacks; with none, to the declared PushReceiver + // and/or a notification per saved subscription that matches it. [deferAck] hands a listener + // that acknowledges messages itself its acknowledgement. + private fun deliverMessage(context: Context, message: PushMessage, deferAck: () -> () -> Unit) { + val matching = listeners.filter { listener -> listener.topics.any { matchesTopic(it, message.topic) } } + // Saved background subscriptions no live one has taken over; they deliver even when a live + // foreground-only subscription on an overlapping filter received the message too. + val entries = synchronized(lock) { saved }.filter { matchesTopic(it.filter, message.topic) } + if (matching.isEmpty() && entries.isEmpty()) { + return + } + for (listener in matching) { + try { + val acknowledging = listener.acknowledgingCallback + if (acknowledging != null) { + acknowledging(message, deferAck()) + } else { + listener.callback(message) + } + } catch (e: Exception) { + report(e) + } + } + val wakeLock = acquireWakeLock(context) + try { + // The app's PushReceiver gets only messages no live callback received. + val handled = matching.isEmpty() && deliverToReceivers(context, message) + val titles = matching.filter { it.background }.map { it.title ?: message.topic } + + if (handled) emptyList() else entries.map { it.title ?: message.topic } + titles.distinct().forEach { notify(context, message, it) } + } finally { + releaseWakeLock(wakeLock) + } + } + + // Call every PushReceiver the app declares. Returns whether one handled the message. + private fun deliverToReceivers(context: Context, message: PushMessage): Boolean { + val intent = Intent(ACTION_MESSAGE).setPackage(context.packageName) + val receivers = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + context.packageManager.queryBroadcastReceivers(intent, PackageManager.ResolveInfoFlags.of(0)) + } else { + @Suppress("DEPRECATION") + context.packageManager.queryBroadcastReceivers(intent, 0) + } + var handled = false + for (info in receivers) { + val name = info.activityInfo?.name ?: continue + try { + val receiver = Class.forName(name, true, context.classLoader).getDeclaredConstructor().newInstance() + if (receiver is PushReceiver && receiver.onMessage(context, message)) { + handled = true + } + } catch (e: Exception) { + report(e) + } + } + return handled + } + + /** Post a notification for [message] that opens the app, with the message in its extras. */ + fun notify(context: Context, message: PushMessage, title: String) { + val manager = NotificationManagerCompat.from(context) + if (!manager.areNotificationsEnabled()) { + return + } + createChannels(context) + val id = 31 * message.hashCode() + title.hashCode() + val builder = NotificationCompat.Builder(context, CHANNEL_ID) + .setContentTitle(title) + .setContentText(message.data) + .setSmallIcon(notificationIcon(context)) + .setAutoCancel(true) + context.packageManager.getLaunchIntentForPackage(context.packageName)?.let { launch -> + launch.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP) + .putExtra(EXTRA_TOPIC, message.topic) + .putExtra(EXTRA_PAYLOAD, message.data) + builder.setContentIntent( + PendingIntent.getActivity(context, id, launch, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE), + ) + } + try { + manager.notify(id, builder.build()) + } catch (e: SecurityException) { + // POST_NOTIFICATIONS not granted; nothing to show. + } + } + + /** The ongoing notification the foreground service shows, on its own quiet channel. */ + fun ongoingNotification(context: Context) = NotificationCompat.Builder(context, SERVICE_CHANNEL_ID) + .also { createChannels(context) } + .setContentTitle(ongoingTitle) + .setContentText("Listening for messages") + .setSmallIcon(notificationIcon(context)) + .setPriority(NotificationCompat.PRIORITY_LOW) + .setOngoing(true) + .build() + + private fun createChannels(context: Context) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + return + } + val manager = context.getSystemService(NotificationManager::class.java) ?: return + manager.createNotificationChannel(NotificationChannel(CHANNEL_ID, channelName, NotificationManager.IMPORTANCE_HIGH)) + manager.createNotificationChannel( + NotificationChannel(SERVICE_CHANNEL_ID, "Background connection", NotificationManager.IMPORTANCE_LOW), + ) + } + + // The icon named by the app's ``, else a + // generic one. + private fun notificationIcon(context: Context): Int { + val metaData = runCatching { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + context.packageManager.getApplicationInfo(context.packageName, PackageManager.ApplicationInfoFlags.of(PackageManager.GET_META_DATA.toLong())) + } else { + @Suppress("DEPRECATION") + context.packageManager.getApplicationInfo(context.packageName, PackageManager.GET_META_DATA) + }.metaData + }.getOrNull() + return metaData?.getInt(NOTIFICATION_ICON, 0)?.takeIf { it != 0 } ?: android.R.drawable.stat_notify_chat + } + + private fun ensureService(context: Context) { + if (!wantsService(context)) { + if (serviceRunning) { + context.stopService(Intent(context, PushService::class.java)) + } + return + } + if (serviceRunning) { + return + } + try { + ContextCompat.startForegroundService(context, Intent(context, PushService::class.java)) + } catch (e: Exception) { + // Android 12+ refuses a foreground-service start from the background; the scheduled + // runs keep delivering, and the next start from the foreground succeeds. + if (!serviceStartReported) { + serviceStartReported = true + report(e) + } + } + } + + // Arm the next run in [delayMs]: the job, and an alarm just after it in case the job is late. + private fun schedule(context: Context, delayMs: Long, jobToo: Boolean = true) { + val interval = if (delayMs == INTERVAL_MS && privileged(context)) PRIVILEGED_INTERVAL_MS else delayMs + if (jobToo) { + try { + val job = JobInfo.Builder(JOB_ID, ComponentName(context, PushJobService::class.java)) + .setMinimumLatency(interval) + .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY) + .setPersisted(true) + .build() + context.getSystemService(JobScheduler::class.java)?.schedule(job) + } catch (e: Exception) { + report(e) + } + } + val alarms = context.getSystemService(AlarmManager::class.java) ?: return + val at = SystemClock.elapsedRealtime() + interval + 1_000L + val intent = tickIntent(context) + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && !alarms.canScheduleExactAlarms()) { + alarms.setAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, at, intent) + } else { + alarms.setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, at, intent) + } + } catch (e: SecurityException) { + alarms.setAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, at, intent) + } + } + + private fun cancelSchedule(context: Context) { + context.getSystemService(JobScheduler::class.java)?.cancel(JOB_ID) + context.getSystemService(AlarmManager::class.java)?.cancel(tickIntent(context)) + } + + private fun tickIntent(context: Context): PendingIntent = PendingIntent.getBroadcast( + context, + 0, + Intent(context, PushAlarmReceiver::class.java).setAction(ACTION_TICK), + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, + ) + + // Allowed exact alarms or exempt from battery optimisation: the alarm is reliable, so the + // runs can be spaced further apart. + private fun privileged(context: Context): Boolean = + Build.VERSION.SDK_INT < Build.VERSION_CODES.S || + context.getSystemService(AlarmManager::class.java)?.canScheduleExactAlarms() == true || + context.getSystemService(PowerManager::class.java)?.isIgnoringBatteryOptimizations(context.packageName) == true + + private fun activeNetwork(context: Context): Network? = + runCatching { context.getSystemService(ConnectivityManager::class.java)?.activeNetwork }.getOrNull() + + private fun networkChanged(context: Context): Boolean { + val now = activeNetwork(context) + return now != null && connectedNetwork != null && now != connectedNetwork + } + + // Reconnect as soon as the default network changes (e.g. from cellular to Wi-Fi) while the + // process is alive, instead of waiting for the next run to notice. + private fun registerNetworkCallback(context: Context) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { + return + } + synchronized(lock) { + if (networkCallback != null) { + return + } + val callback = object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) { + worker.execute { + if (mqtt != null && network != connectedNetwork) { + converge(context, heartbeat = false) + } + } + } + } + try { + context.getSystemService(ConnectivityManager::class.java)?.registerDefaultNetworkCallback(callback) + networkCallback = callback + } catch (e: Exception) { + report(e) + } + } + } + + private fun unregisterNetworkCallback(context: Context) { + val callback = synchronized(lock) { networkCallback.also { networkCallback = null } } ?: return + runCatching { context.getSystemService(ConnectivityManager::class.java)?.unregisterNetworkCallback(callback) } + } + + private fun acquireWakeLock(context: Context): PowerManager.WakeLock? = runCatching { + context.getSystemService(PowerManager::class.java) + ?.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Appwrite:push") + ?.apply { + setReferenceCounted(false) + acquire(WAKE_LOCK_MS) + } + }.getOrNull() + + private fun releaseWakeLock(wakeLock: PowerManager.WakeLock?) { + runCatching { + if (wakeLock?.isHeld == true) { + wakeLock.release() + } + } + } +} + +/** + * Background push state in the app's no-backup directory, so a restored backup on another + * device never carries this install's id or credential: the saved subscriptions with their + * connection config (cleared when background delivery stops), and the settings that outlive + * it (the install id, the foreground-mode choice, a refusal waiting for onError). + */ +internal object PushStore { + private const val STATE_FILE = "appwrite_push_state.json" + private const val SETTINGS_FILE = "appwrite_push_settings.json" + + fun loadState(context: Context): Pair>? = synchronized(this) { + val json = read(context, STATE_FILE) ?: return null + runCatching { + val config = json.getJSONObject("config").let { + PushConfig( + host = it.getString("host"), + port = it.getInt("port"), + tls = it.getBoolean("tls"), + tlsInsecure = it.getBoolean("tlsInsecure"), + clientId = it.getString("clientId"), + keepAlive = it.getInt("keepAlive"), + authMethod = it.getString("authMethod"), + credential = it.getString("credential"), + project = it.getString("project"), + ) + } + val list = json.getJSONArray("entries") + val entries = (0 until list.length()).map { index -> + val entry = list.getJSONObject(index) + PushEntry( + filter = entry.getString("filter"), + retry = entry.getBoolean("retry"), + title = if (entry.isNull("title")) null else entry.getString("title"), + ) + } + config to entries + }.getOrNull() + } + + fun saveState(context: Context, config: PushConfig, entries: List) = synchronized(this) { + val json = JSONObject() + .put( + "config", + JSONObject() + .put("host", config.host) + .put("port", config.port) + .put("tls", config.tls) + .put("tlsInsecure", config.tlsInsecure) + .put("clientId", config.clientId) + .put("keepAlive", config.keepAlive) + .put("authMethod", config.authMethod) + .put("credential", config.credential) + .put("project", config.project), + ) + .put( + "entries", + JSONArray(entries.map { JSONObject().put("filter", it.filter).put("retry", it.retry).put("title", it.title ?: JSONObject.NULL) }), + ) + write(context, STATE_FILE, json) + } + + fun clearState(context: Context) = synchronized(this) { + file(context, STATE_FILE).delete() + } + + /** A random id for this install, created once, which keeps the broker's replay position. */ + fun installId(context: Context): String = synchronized(this) { + val settings = read(context, SETTINGS_FILE) ?: JSONObject() + settings.optString("installId").ifEmpty { + val id = UUID.randomUUID().toString() + write(context, SETTINGS_FILE, settings.put("installId", id)) + id + } + } + + fun foreground(context: Context): Boolean = synchronized(this) { + read(context, SETTINGS_FILE)?.optBoolean("foreground", false) ?: false + } + + fun setForeground(context: Context, enabled: Boolean) = synchronized(this) { + write(context, SETTINGS_FILE, (read(context, SETTINGS_FILE) ?: JSONObject()).put("foreground", enabled)) + } + + /** The saved adaptive heartbeat; the default interval until one was saved. */ + fun heartbeat(context: Context): PushHeartbeat = synchronized(this) { + val json = read(context, SETTINGS_FILE)?.optJSONObject("heartbeat") ?: return PushHeartbeat(PushBackground.HEARTBEAT_MS) + PushHeartbeat( + intervalMs = json.optLong("intervalMs", PushBackground.HEARTBEAT_MS), + failures = json.optInt("failures", 0), + shrunkAt = json.optLong("shrunkAt", 0L), + ) + } + + fun setHeartbeat(context: Context, state: PushHeartbeat) = synchronized(this) { + val heartbeat = JSONObject() + .put("intervalMs", state.intervalMs) + .put("failures", state.failures) + .put("shrunkAt", state.shrunkAt) + write(context, SETTINGS_FILE, (read(context, SETTINGS_FILE) ?: JSONObject()).put("heartbeat", heartbeat)) + } + + fun setStoppedError(context: Context, message: String) = synchronized(this) { + write(context, SETTINGS_FILE, (read(context, SETTINGS_FILE) ?: JSONObject()).put("stoppedError", message)) + } + + /** The refusal that stopped background delivery while no onError was registered, once. */ + fun takeStoppedError(context: Context): String? = synchronized(this) { + val settings = read(context, SETTINGS_FILE) ?: return null + val message = settings.optString("stoppedError").ifEmpty { return null } + settings.remove("stoppedError") + write(context, SETTINGS_FILE, settings) + message + } + + private fun file(context: Context, name: String) = AtomicFile(File(context.noBackupFilesDir, name)) + + private fun read(context: Context, name: String): JSONObject? = + runCatching { JSONObject(String(file(context, name).readFully(), Charsets.UTF_8)) }.getOrNull() + + private fun write(context: Context, name: String, json: JSONObject) { + val file = file(context, name) + val out = try { + file.startWrite() + } catch (e: IOException) { + Log.e(LOG_TAG, "Could not save push state", e) + return + } + try { + out.write(json.toString().toByteArray(Charsets.UTF_8)) + file.finishWrite(out) + } catch (e: IOException) { + file.failWrite(out) + Log.e(LOG_TAG, "Could not save push state", e) + } + } +} diff --git a/android/src/main/java/io/appwrite/services/PushBridge.kt b/android/src/main/java/io/appwrite/services/PushBridge.kt new file mode 100644 index 00000000..d7b79b5d --- /dev/null +++ b/android/src/main/java/io/appwrite/services/PushBridge.kt @@ -0,0 +1,135 @@ +package io.appwrite.services + +import android.content.Context +import android.os.SystemClock +import org.json.JSONArray +import org.json.JSONObject +import java.util.UUID +import java.util.concurrent.ConcurrentHashMap + +/** + * The contract the React Native and Flutter SDKs use to host their background push + * subscriptions on this Android core, so they get the same delivery as the Android SDK: saved + * subscriptions, scheduled wake-ups after the process dies, notifications, and foreground mode. + * + * Both SDKs pass the same JSON: [host] takes the connection config and every live subscription, + * and each message for a live subscription comes back through [Events.onMessage] with its id and + * an acknowledgement token, which the SDK passes to [ack] once its callback has run (the broker's + * PUBACK waits for it, at most ten seconds). One bridge per process; hosting again replaces the + * live subscriptions. + */ +class PushBridge(context: Context, private val events: Events) { + /** Where messages and errors go; called on a background thread. */ + interface Events { + fun onMessage(subscriptionId: String, message: PushMessage, ackToken: String) + + fun onError(message: String) + } + + private val appContext = context.applicationContext + // Acknowledgement token -> (when it was handed out, the acknowledgement). A token the SDK never + // acknowledges (its JS or Dart side was gone) is dropped once the core has acknowledged the + // message anyway, so the map stays bounded. + private val acks = ConcurrentHashMap Unit>>() + + // Whether the app registered onError; without one, errors are logged and a refused + // credential is kept for the next registration. + @Volatile + private var errorCallback = false + + /** + * Host the subscriptions in [subscriptionsJson] on the background connection described by + * [configJson]. + * + * Config: `{"host", "port", "tls", "tlsInsecure", "clientId" (optional; per user and install + * when empty), "authMethod", "credential", "project"}`. Subscriptions: an array of + * `{"id", "topic", "background", "title" (optional), "retry"}`. + * + * [done] is called once the connection is up and every filter is subscribed, or with the + * failure's message; the SDK rejects its subscribe with it and reports it itself. + */ + fun host(configJson: String, subscriptionsJson: String, done: (String?) -> Unit) { + val json = JSONObject(configJson) + val authMethod = json.getString("authMethod") + val credential = json.getString("credential") + val config = PushConfig( + host = json.getString("host"), + port = json.getInt("port"), + tls = json.optBoolean("tls", false), + tlsInsecure = json.optBoolean("tlsInsecure", false), + clientId = json.optString("clientId").ifEmpty { defaultPushClientId(appContext, authMethod, credential) }, + keepAlive = KEEP_ALIVE_SECONDS, + authMethod = authMethod, + credential = credential, + project = json.optString("project"), + ) + val list = JSONArray(subscriptionsJson) + val listeners = (0 until list.length()).map { index -> + val subscription = list.getJSONObject(index) + val id = subscription.getString("id") + PushListener( + topics = setOf(subscription.getString("topic")), + callback = {}, + acknowledgingCallback = { message, ack -> + val token = UUID.randomUUID().toString() + val now = SystemClock.elapsedRealtime() + acks.entries.removeIf { now - it.value.first > STALE_ACK_MS } + acks[token] = now to ack + events.onMessage(id, message, token) + }, + background = subscription.optBoolean("background", false), + title = if (subscription.isNull("title")) null else subscription.optString("title").ifEmpty { null }, + retry = subscription.optBoolean("retry", true), + ) + } + PushBackground.errorHandler = { error -> + if (errorCallback) { + events.onError(error.message ?: error.toString()) + } + errorCallback + } + PushBackground.host(appContext, config, listeners) { error -> + done(error?.let { it.message ?: it.toString() }) + } + } + + /** Acknowledge a message once the SDK's callback for it has run. */ + fun ack(token: String) { + acks.remove(token)?.second?.invoke() + } + + /** Stop hosting the live subscriptions, keeping the ones saved by an earlier run. */ + fun release() = PushBackground.release(appContext) + + /** Stop background delivery and forget every saved subscription (sign-out). */ + fun stop() = PushBackground.stop(appContext) + + /** Run background delivery in a foreground service; saved across restarts. */ + fun setForeground(enabled: Boolean) = PushBackground.setForeground(appContext, enabled) + + /** Whether an earlier run saved background subscriptions that are still delivered. */ + fun hasSaved(): Boolean = PushBackground.hasSaved(appContext) + + /** Resume saved background delivery now instead of at the next scheduled run. */ + fun resume() = PushBackground.resume(appContext) + + /** + * Record whether the app has an onError callback. Returns the refusal that stopped background + * delivery while it had none, once, so the app can deliver it now. + */ + fun setErrorCallback(registered: Boolean): String? { + errorCallback = registered + return if (registered) PushStore.takeStoppedError(appContext) else null + } + + /** The default client id for a credential, so a foreground connection shares its session. */ + fun defaultClientId(authMethod: String, credential: String): String = + defaultPushClientId(appContext, authMethod, credential) + + private companion object { + const val KEEP_ALIVE_SECONDS = 60 + + // Past the core's ten-second acknowledgement cap, after which the message was acknowledged. + const val STALE_ACK_MS = 30_000L + } +} diff --git a/android/src/main/java/io/appwrite/services/PushCore.kt b/android/src/main/java/io/appwrite/services/PushCore.kt new file mode 100644 index 00000000..20c56242 --- /dev/null +++ b/android/src/main/java/io/appwrite/services/PushCore.kt @@ -0,0 +1,374 @@ +package io.appwrite.services + +import android.content.Context +import android.os.Build +import android.util.Base64 +import com.hivemq.client.mqtt.MqttClient +import com.hivemq.client.mqtt.datatypes.MqttUtf8String +import com.hivemq.client.mqtt.lifecycle.MqttClientDisconnectedContext +import com.hivemq.client.mqtt.lifecycle.MqttDisconnectSource +import com.hivemq.client.mqtt.mqtt5.Mqtt5AsyncClient +import com.hivemq.client.mqtt.mqtt5.Mqtt5ClientConfig +import com.hivemq.client.mqtt.mqtt5.auth.Mqtt5EnhancedAuthMechanism +import com.hivemq.client.mqtt.mqtt5.exceptions.Mqtt5ConnAckException +import com.hivemq.client.mqtt.mqtt5.exceptions.Mqtt5DisconnectException +import com.hivemq.client.mqtt.mqtt5.message.auth.Mqtt5Auth +import com.hivemq.client.mqtt.mqtt5.message.auth.Mqtt5AuthBuilder +import com.hivemq.client.mqtt.mqtt5.message.auth.Mqtt5EnhancedAuthBuilder +import com.hivemq.client.mqtt.mqtt5.message.connect.Mqtt5Connect +import com.hivemq.client.mqtt.mqtt5.message.connect.connack.Mqtt5ConnAck +import com.hivemq.client.mqtt.mqtt5.message.connect.connack.Mqtt5ConnAckReasonCode +import com.hivemq.client.mqtt.mqtt5.message.disconnect.Mqtt5Disconnect +import com.hivemq.client.mqtt.mqtt5.message.disconnect.Mqtt5DisconnectReasonCode +import com.hivemq.client.mqtt.mqtt5.message.publish.Mqtt5Publish +import io.appwrite.exceptions.AppwriteException +import org.json.JSONObject +import java.security.KeyStore +import java.security.Provider +import java.security.cert.X509Certificate +import java.util.concurrent.CompletableFuture +import java.util.concurrent.ExecutionException +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean +import javax.net.ssl.ManagerFactoryParameters +import javax.net.ssl.TrustManager +import javax.net.ssl.TrustManagerFactory +import javax.net.ssl.TrustManagerFactorySpi +import javax.net.ssl.X509TrustManager + +/** + * A message delivered on a subscribed topic — the MQTT push analog of a realtime event. + */ +data class PushMessage( + val topic: String, + val payload: ByteArray, + val qos: Int, +) { + /** The payload decoded as UTF-8 text, as it was sent. */ + val data: String + get() = String(payload, Charsets.UTF_8) +} + +/** Connection parameters shared between the in-process client and the foreground service. */ +internal data class PushConfig( + val host: String, + val port: Int, + val tls: Boolean, + val tlsInsecure: Boolean, + val clientId: String, + val keepAlive: Int, + val authMethod: String, + val credential: String, + val project: String, +) + +/** + * The userId claim from an Appwrite JWT, used as the default MQTT client id so the + * broker's replay cursor is stable per user. Returns "" when the token is not a decodable JWT + * (e.g. a session secret), so the caller can fall back to the raw credential. + */ +internal fun userIdFromJwt(jwt: String): String { + val parts = jwt.split('.') + if (parts.size < 2) { + return "" + } + return runCatching { + val json = String( + Base64.decode(parts[1], Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING), + Charsets.UTF_8, + ) + JSONObject(json).optString("userId", "") + }.getOrDefault("") +} + +/** The user `id` from an Appwrite session secret, or "" when it is not decodable. */ +internal fun userIdFromSession(session: String): String = runCatching { + val json = String(Base64.decode(session, Base64.DEFAULT), Charsets.UTF_8) + JSONObject(json).optString("id", "") +}.getOrDefault("") + +/** + * The default MQTT client id: one per user and install (`-`, or the install id + * alone when the credential names no user). The broker keeps the replay position per user and + * client id, so each device resumes its own backlog across restarts, and two devices of one user + * do not take over each other's session. + */ +internal fun defaultPushClientId(context: Context, authMethod: String, credential: String): String { + val userId = if (authMethod == "appwrite-session") userIdFromSession(credential) else userIdFromJwt(credential) + val installId = PushStore.installId(context) + return if (userId.isEmpty()) installId else "$userId-$installId" +} + +/** + * Build (but do not connect) an MQTT 5 client from [config]. [onError] receives every connection + * error (a lost connection, a failed automatic reconnect, a broker DISCONNECT) except a refused + * initial CONNECT, which [pushConnect] throws instead. + */ +internal fun buildPushClient( + config: PushConfig, + onConnected: (() -> Unit)? = null, + onDisconnected: (() -> Unit)? = null, + onError: ((Throwable) -> Unit)? = null, + onAuthRefused: ((Throwable) -> Unit)? = null, +): Mqtt5AsyncClient { + // config.clientId is stable per user (see buildConfig), so a reconnect rebuild reuses it + // and resumes the same session. + var builder = MqttClient.builder() + .useMqttVersion5() + .serverHost(config.host) + .serverPort(config.port) + // Recover from an unexpected disconnect (network drop, broker restart) with HiveMQ's + // exponential backoff. A user-initiated disconnect() (see close()) does not trigger it. + // Because clean start is off and the broker resumes the same session, subscriptions and + // the QoS-1 replay cursor persist, so delivery continues without resubscribing (which + // would stack a second per-subscription callback). + .automaticReconnectWithDefaultConfig() + + if (config.clientId.isNotEmpty()) { + builder = builder.identifier(config.clientId) + } + + // Whether this client has connected yet: until it has, a refused CONNECT is final. + val connected = AtomicBoolean(false) + builder = builder.addConnectedListener { + connected.set(true) + onConnected?.invoke() + } + builder = builder.addDisconnectedListener { context -> + // A refused first CONNECT (bad credential, rate limit) is final: stop the automatic + // reconnect so pushConnect fails with the broker's reason instead of retrying forever. + val refused = !connected.get() && context.cause is Mqtt5ConnAckException + // With [onAuthRefused], a credential refused later (a failed automatic reconnect or + // re-auth) is final too, instead of being retried with the same credential forever. + val authRefused = onAuthRefused != null && connected.get() && + context.source != MqttDisconnectSource.USER && isAuthRefusal(context.cause) + if (refused || authRefused) { + context.reconnector.reconnect(false) + } + onDisconnected?.invoke() + if (!refused) { + val error = pushDisconnectError(context) + if (authRefused && error != null) { + // Reported by onAuthRefused, which also stops for good. + onAuthRefused?.invoke(error) + } else { + error?.let { onError?.invoke(it) } + } + } + } + + if (config.tls) { + builder = if (config.tlsInsecure) { + builder.sslConfig() + .trustManagerFactory(PushInsecureTrustManagerFactory) + .hostnameVerifier { _, _ -> true } + .applySslConfig() + } else { + builder.sslWithDefaultConfig() + } + } + + return builder.buildAsync() +} + +/** + * Connect with Appwrite enhanced auth; blocks until CONNACK. Throws an + * [AppwriteException] on failure, whose message is the broker's MQTT 5 + * Reason String when it refused the CONNECT with one. + */ +internal fun Mqtt5AsyncClient.pushConnect(config: PushConfig) { + try { + sendPushConnect(config) + } catch (e: Exception) { + throw connectError((e as? ExecutionException)?.cause ?: e) + } +} + +/** The error a failed CONNECT reports: the broker's Reason String when it refused one. */ +internal fun connectError(cause: Throwable): AppwriteException = + if (cause is Mqtt5ConnAckException) { + connAckError(cause) + } else { + AppwriteException(cause.message ?: "MQTT connection failed") + } + +/** + * Whether [error] is the broker refusing the credential (a CONNECT or re-auth), which retrying + * with the same credential cannot fix, unlike a rate limit or a network failure. + */ +internal fun isAuthRefusal(error: Throwable?): Boolean = when (error) { + is Mqtt5ConnAckException -> error.mqttMessage.reasonCode in setOf( + Mqtt5ConnAckReasonCode.NOT_AUTHORIZED, + Mqtt5ConnAckReasonCode.BAD_USER_NAME_OR_PASSWORD, + Mqtt5ConnAckReasonCode.BAD_AUTHENTICATION_METHOD, + Mqtt5ConnAckReasonCode.BANNED, + ) + is Mqtt5DisconnectException -> error.mqttMessage.reasonCode == Mqtt5DisconnectReasonCode.NOT_AUTHORIZED + else -> false +} + +/** Send the CONNECT and wait for the CONNACK, at most [timeoutSeconds] when given. */ +internal fun Mqtt5AsyncClient.sendPushConnect(config: PushConfig, timeoutSeconds: Long? = null) { + val connAck = connectWith() + .enhancedAuth(PushAuthMechanism(config.authMethod, config.credential)) + // Clean start is always off, so the broker keeps this client's session and can + // redeliver missed messages to QoS-1 subscriptions on reconnect. + .cleanStart(false) + .keepAlive(config.keepAlive) + .userProperties() + .add("projectId", config.project) + .applyUserProperties() + .send() + if (timeoutSeconds == null) { + connAck.get() + } else { + connAck.get(timeoutSeconds, TimeUnit.SECONDS) + } +} + +/** + * The error a lost connection reports, or null when it is not an error (a disconnect the app + * asked for, or a broker DISCONNECT with a success reason code). The broker explains a refused + * CONNECT or a DISCONNECT it sends with an MQTT 5 Reason String, which becomes the message. + */ +internal fun pushDisconnectError(context: MqttClientDisconnectedContext): Throwable? { + val cause = context.cause + return when { + context.source == MqttDisconnectSource.USER -> null + cause is Mqtt5ConnAckException -> connAckError(cause) + cause is Mqtt5DisconnectException && context.source == MqttDisconnectSource.SERVER -> { + val disconnect = cause.mqttMessage + if (disconnect.reasonCode.isError) { + AppwriteException( + disconnect.reasonString.map { it.toString() } + .orElse("Disconnected by the broker (reason ${disconnect.reasonCode})"), + ) + } else { + null + } + } + else -> cause + } +} + +/** A refused CONNECT, with the CONNACK Reason String as its message when the broker sent one. */ +internal fun connAckError(e: Mqtt5ConnAckException): AppwriteException = + AppwriteException( + e.mqttMessage.reasonString.map { it.toString() }.orElse(e.message ?: "MQTT connection refused"), + ) + +internal fun Mqtt5Publish.toPushMessage(): PushMessage = + PushMessage( + topic = topic.toString(), + payload = payloadAsBytes, + qos = qos.code, + ) + +/** + * Single-step MQTT 5 enhanced auth: the method and credential are placed in the CONNECT + * packet and the broker accepts them in the CONNACK, with no AUTH round-trip. + */ +internal class PushAuthMechanism( + private val method: String, + private val credential: String, +) : Mqtt5EnhancedAuthMechanism { + + override fun getMethod(): MqttUtf8String = MqttUtf8String.of(method) + + override fun getTimeout(): Int = 30 + + override fun onAuth( + clientConfig: Mqtt5ClientConfig, + connect: Mqtt5Connect, + authBuilder: Mqtt5EnhancedAuthBuilder, + ): CompletableFuture { + authBuilder.data(credential.toByteArray(Charsets.UTF_8)) + return CompletableFuture.completedFuture(null) + } + + override fun onReAuth( + clientConfig: Mqtt5ClientConfig, + authBuilder: Mqtt5AuthBuilder, + ): CompletableFuture { + authBuilder.data(credential.toByteArray(Charsets.UTF_8)) + return CompletableFuture.completedFuture(null) + } + + override fun onContinue( + clientConfig: Mqtt5ClientConfig, + auth: Mqtt5Auth, + authBuilder: Mqtt5AuthBuilder, + ): CompletableFuture = CompletableFuture.completedFuture(true) + + override fun onAuthSuccess( + clientConfig: Mqtt5ClientConfig, + connAck: Mqtt5ConnAck, + ): CompletableFuture = CompletableFuture.completedFuture(true) + + override fun onReAuthSuccess( + clientConfig: Mqtt5ClientConfig, + auth: Mqtt5Auth, + ): CompletableFuture = CompletableFuture.completedFuture(true) + + override fun onAuthRejected(clientConfig: Mqtt5ClientConfig, connAck: Mqtt5ConnAck) = Unit + + override fun onReAuthRejected(clientConfig: Mqtt5ClientConfig, disconnect: Mqtt5Disconnect) = Unit + + override fun onAuthError(clientConfig: Mqtt5ClientConfig, cause: Throwable) = Unit + + override fun onReAuthError(clientConfig: Mqtt5ClientConfig, cause: Throwable) = Unit +} + +/** + * A [TrustManagerFactory] that accepts any certificate — used when `tlsInsecure` is set + * (for a broker whose certificate does not chain to a public root). + */ +internal object PushInsecureTrustManagerFactory : TrustManagerFactory( + object : TrustManagerFactorySpi() { + override fun engineInit(keyStore: KeyStore?) = Unit + + override fun engineInit(parameters: ManagerFactoryParameters?) = Unit + + override fun engineGetTrustManagers(): Array = arrayOf( + object : X509TrustManager { + override fun checkClientTrusted(chain: Array?, authType: String?) = Unit + + override fun checkServerTrusted(chain: Array?, authType: String?) = Unit + + override fun getAcceptedIssuers(): Array = emptyArray() + }, + ) + }, + @Suppress("DEPRECATION") object : Provider("AppwriteInsecure", 1.0, "insecure trust-all") {}, + "AppwriteInsecure", +) + +/** A live in-app subscription hosted in the background: its topics, callback, notification opt-in and QoS. */ +internal class PushListener( + val topics: Set, + val callback: (PushMessage) -> Unit, + val background: Boolean, + val title: String?, + val retry: Boolean, + // Called instead of [callback] when set, with the acknowledgement to run once the message was + // handled: the React Native and Flutter bridges acknowledge after their callback has run. + val acknowledgingCallback: ((PushMessage, () -> Unit) -> Unit)? = null, +) + +/** MQTT topic-filter match with '+' (single level) and '#' (multi level). */ +internal fun matchesTopic(filter: String, topic: String): Boolean { + val filterParts = filter.split('/') + val topicParts = topic.split('/') + for (i in filterParts.indices) { + if (filterParts[i] == "#") { + return true + } + if (i >= topicParts.size) { + return false + } + if (filterParts[i] != "+" && filterParts[i] != topicParts[i]) { + return false + } + } + return filterParts.size == topicParts.size +} diff --git a/android/src/main/java/io/appwrite/services/PushReceiver.kt b/android/src/main/java/io/appwrite/services/PushReceiver.kt new file mode 100644 index 00000000..92d04727 --- /dev/null +++ b/android/src/main/java/io/appwrite/services/PushReceiver.kt @@ -0,0 +1,33 @@ +package io.appwrite.services + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent + +/** + * Handles background push messages in code when no in-app callback is listening, e.g. after + * the process was started in the background to reconnect. Subclass it and declare it in your + * manifest with the push message action: + * + * ```xml + * + * + * + * + * + * ``` + * + * The SDK calls [onMessage] directly on a background thread, under a wakelock of about ten + * seconds, so hand longer work to WorkManager. Messages a live callback receives do not + * reach it. + */ +abstract class PushReceiver : BroadcastReceiver() { + /** + * Handle [message]. Return true when you handled it yourself, so the SDK does not post its + * default notification; return false to have it posted as well. + */ + abstract fun onMessage(context: Context, message: PushMessage): Boolean + + // Messages arrive through onMessage, never as a broadcast. + final override fun onReceive(context: Context, intent: Intent) = Unit +} diff --git a/android/src/main/java/io/appwrite/services/PushService.kt b/android/src/main/java/io/appwrite/services/PushService.kt new file mode 100644 index 00000000..e2a05f89 --- /dev/null +++ b/android/src/main/java/io/appwrite/services/PushService.kt @@ -0,0 +1,67 @@ +package io.appwrite.services + +import android.app.Service +import android.content.Intent +import android.content.pm.ServiceInfo +import android.os.Build +import android.os.IBinder +import androidx.core.app.ServiceCompat + +/** + * The foreground mode of background push delivery ([Push.setForeground]): a foreground service + * that keeps the process at foreground priority, so the connection held by the process stays + * up and delivers immediately, also during Doze. The system restarts it after a kill, and it + * restores the saved subscriptions. Started by the SDK; not meant to be started directly. + */ +class PushService : Service() { + override fun onBind(intent: Intent?): IBinder? = null + + override fun onCreate() { + super.onCreate() + PushBackground.serviceRunning = true + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + if (!PushBackground.wantsService(this)) { + stopSelf() + return START_NOT_STICKY + } + try { + startForegroundCompat() + } catch (e: Exception) { + // E.g. a background start refused on Android 12+; the scheduled runs keep delivering. + stopSelf() + PushBackground.report(e) + return START_NOT_STICKY + } + PushBackground.tick(this) {} + // Recreated after a kill with a null intent; the saved subscriptions are restored then. + return START_STICKY + } + + private fun startForegroundCompat() { + val notification = PushBackground.ongoingNotification(this) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + ServiceCompat.startForeground( + this, + PushBackground.NOTIFICATION_ID, + notification, + ServiceInfo.FOREGROUND_SERVICE_TYPE_REMOTE_MESSAGING, + ) + } else { + startForeground(PushBackground.NOTIFICATION_ID, notification) + } + } + + // Some devices kill the process shortly after its task is swiped away; a run a few seconds + // later restarts the service. + override fun onTaskRemoved(rootIntent: Intent?) { + PushBackground.scheduleRestart(this) + super.onTaskRemoved(rootIntent) + } + + override fun onDestroy() { + PushBackground.serviceRunning = false + super.onDestroy() + } +} diff --git a/android/src/main/java/io/appwrite/services/PushWakeups.kt b/android/src/main/java/io/appwrite/services/PushWakeups.kt new file mode 100644 index 00000000..091fd528 --- /dev/null +++ b/android/src/main/java/io/appwrite/services/PushWakeups.kt @@ -0,0 +1,46 @@ +package io.appwrite.services + +import android.app.job.JobParameters +import android.app.job.JobService +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent + +/** + * The scheduled run of background push delivery: reconnects and re-arms itself. Scheduled + * by the SDK; not meant to be started directly. + */ +class PushJobService : JobService() { + override fun onStartJob(params: JobParameters): Boolean { + PushBackground.tick(this) { jobFinished(params, false) } + return true + } + + // The run re-arms the next one itself, so a stopped run needs no retry. + override fun onStopJob(params: JobParameters): Boolean = false +} + +/** + * The alarm that backs up the scheduled job when it runs late. Scheduled by the SDK; not meant + * to be sent directly. + */ +class PushAlarmReceiver : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + if (intent.action != PushBackground.ACTION_TICK) { + return + } + val pending = goAsync() + PushBackground.tick(context) { pending.finish() } + } +} + +/** Resumes background push delivery after a reboot or an app update. */ +class PushBootReceiver : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + if (intent.action != Intent.ACTION_BOOT_COMPLETED && intent.action != Intent.ACTION_MY_PACKAGE_REPLACED) { + return + } + val pending = goAsync() + PushBackground.tick(context) { pending.finish() } + } +} diff --git a/docs/examples/account/create-email-password-session.md b/docs/examples/account/create-email-password-session.md index e38beaf7..f52da467 100644 --- a/docs/examples/account/create-email-password-session.md +++ b/docs/examples/account/create-email-password-session.md @@ -10,6 +10,7 @@ const account = new Account(client); const result = await account.createEmailPasswordSession({ email: 'email@example.com', password: 'password', + duration: 60, // optional }); console.log(result); diff --git a/docs/examples/account/delete-sessions.md b/docs/examples/account/delete-sessions.md index 0f1bc281..60045053 100644 --- a/docs/examples/account/delete-sessions.md +++ b/docs/examples/account/delete-sessions.md @@ -7,7 +7,9 @@ const client = new Client() const account = new Account(client); -const result = await account.deleteSessions(); +const result = await account.deleteSessions({ + current: false, // optional +}); console.log(result); ``` diff --git a/docs/examples/account/list-sessions.md b/docs/examples/account/list-sessions.md index c8acda4d..ddf9c133 100644 --- a/docs/examples/account/list-sessions.md +++ b/docs/examples/account/list-sessions.md @@ -7,7 +7,9 @@ const client = new Client() const account = new Account(client); -const result = await account.listSessions(); +const result = await account.listSessions({ + total: false, // optional +}); console.log(result); ``` diff --git a/docs/examples/apps/create-installation-token.md b/docs/examples/apps/create-installation-token.md new file mode 100644 index 00000000..ff84fa28 --- /dev/null +++ b/docs/examples/apps/create-installation-token.md @@ -0,0 +1,16 @@ +```javascript +import { Client, Apps } from 'react-native-appwrite'; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject(''); // Your project ID + +const apps = new Apps(client); + +const result = await apps.createInstallationToken({ + appId: '', + installationId: '', +}); + +console.log(result); +``` diff --git a/docs/examples/apps/create-key.md b/docs/examples/apps/create-key.md new file mode 100644 index 00000000..c36c4696 --- /dev/null +++ b/docs/examples/apps/create-key.md @@ -0,0 +1,15 @@ +```javascript +import { Client, Apps } from 'react-native-appwrite'; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject(''); // Your project ID + +const apps = new Apps(client); + +const result = await apps.createKey({ + appId: '', +}); + +console.log(result); +``` diff --git a/docs/examples/apps/create-secret.md b/docs/examples/apps/create-secret.md new file mode 100644 index 00000000..d994b74f --- /dev/null +++ b/docs/examples/apps/create-secret.md @@ -0,0 +1,15 @@ +```javascript +import { Client, Apps } from 'react-native-appwrite'; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject(''); // Your project ID + +const apps = new Apps(client); + +const result = await apps.createSecret({ + appId: '', +}); + +console.log(result); +``` diff --git a/docs/examples/apps/create.md b/docs/examples/apps/create.md new file mode 100644 index 00000000..f472d846 --- /dev/null +++ b/docs/examples/apps/create.md @@ -0,0 +1,33 @@ +```javascript +import { Client, Apps } from 'react-native-appwrite'; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject(''); // Your project ID + +const apps = new Apps(client); + +const result = await apps.create({ + appId: '', + name: '', + redirectUris: [], + description: '', // optional + clientUri: 'https://example.com', // optional + logoUri: 'https://example.com', // optional + privacyPolicyUrl: 'https://example.com', // optional + termsUrl: 'https://example.com', // optional + contacts: [], // optional + tagline: '', // optional + tags: [], // optional + images: [], // optional + supportUrl: 'https://example.com', // optional + dataDeletionUrl: 'https://example.com', // optional + postLogoutRedirectUris: [], // optional + enabled: false, // optional + type: 'public', // optional + deviceFlow: false, // optional + teamId: '', // optional +}); + +console.log(result); +``` diff --git a/docs/examples/apps/delete-installation.md b/docs/examples/apps/delete-installation.md new file mode 100644 index 00000000..f8cfa235 --- /dev/null +++ b/docs/examples/apps/delete-installation.md @@ -0,0 +1,16 @@ +```javascript +import { Client, Apps } from 'react-native-appwrite'; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject(''); // Your project ID + +const apps = new Apps(client); + +const result = await apps.deleteInstallation({ + appId: '', + installationId: '', +}); + +console.log(result); +``` diff --git a/docs/examples/apps/delete-key.md b/docs/examples/apps/delete-key.md new file mode 100644 index 00000000..a7c8b88a --- /dev/null +++ b/docs/examples/apps/delete-key.md @@ -0,0 +1,16 @@ +```javascript +import { Client, Apps } from 'react-native-appwrite'; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject(''); // Your project ID + +const apps = new Apps(client); + +const result = await apps.deleteKey({ + appId: '', + keyId: '', +}); + +console.log(result); +``` diff --git a/docs/examples/apps/delete-secret.md b/docs/examples/apps/delete-secret.md new file mode 100644 index 00000000..fe82ce48 --- /dev/null +++ b/docs/examples/apps/delete-secret.md @@ -0,0 +1,16 @@ +```javascript +import { Client, Apps } from 'react-native-appwrite'; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject(''); // Your project ID + +const apps = new Apps(client); + +const result = await apps.deleteSecret({ + appId: '', + secretId: '', +}); + +console.log(result); +``` diff --git a/docs/examples/apps/delete-tokens.md b/docs/examples/apps/delete-tokens.md new file mode 100644 index 00000000..06386ae1 --- /dev/null +++ b/docs/examples/apps/delete-tokens.md @@ -0,0 +1,15 @@ +```javascript +import { Client, Apps } from 'react-native-appwrite'; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject(''); // Your project ID + +const apps = new Apps(client); + +const result = await apps.deleteTokens({ + appId: '', +}); + +console.log(result); +``` diff --git a/docs/examples/apps/delete.md b/docs/examples/apps/delete.md new file mode 100644 index 00000000..ef20501c --- /dev/null +++ b/docs/examples/apps/delete.md @@ -0,0 +1,15 @@ +```javascript +import { Client, Apps } from 'react-native-appwrite'; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject(''); // Your project ID + +const apps = new Apps(client); + +const result = await apps.delete({ + appId: '', +}); + +console.log(result); +``` diff --git a/docs/examples/apps/get-installation.md b/docs/examples/apps/get-installation.md new file mode 100644 index 00000000..627a02a1 --- /dev/null +++ b/docs/examples/apps/get-installation.md @@ -0,0 +1,16 @@ +```javascript +import { Client, Apps } from 'react-native-appwrite'; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject(''); // Your project ID + +const apps = new Apps(client); + +const result = await apps.getInstallation({ + appId: '', + installationId: '', +}); + +console.log(result); +``` diff --git a/docs/examples/apps/get-key.md b/docs/examples/apps/get-key.md new file mode 100644 index 00000000..8123a459 --- /dev/null +++ b/docs/examples/apps/get-key.md @@ -0,0 +1,16 @@ +```javascript +import { Client, Apps } from 'react-native-appwrite'; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject(''); // Your project ID + +const apps = new Apps(client); + +const result = await apps.getKey({ + appId: '', + keyId: '', +}); + +console.log(result); +``` diff --git a/docs/examples/apps/get-secret.md b/docs/examples/apps/get-secret.md new file mode 100644 index 00000000..aae0a5a7 --- /dev/null +++ b/docs/examples/apps/get-secret.md @@ -0,0 +1,16 @@ +```javascript +import { Client, Apps } from 'react-native-appwrite'; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject(''); // Your project ID + +const apps = new Apps(client); + +const result = await apps.getSecret({ + appId: '', + secretId: '', +}); + +console.log(result); +``` diff --git a/docs/examples/apps/get.md b/docs/examples/apps/get.md new file mode 100644 index 00000000..80507f03 --- /dev/null +++ b/docs/examples/apps/get.md @@ -0,0 +1,15 @@ +```javascript +import { Client, Apps } from 'react-native-appwrite'; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject(''); // Your project ID + +const apps = new Apps(client); + +const result = await apps.get({ + appId: '', +}); + +console.log(result); +``` diff --git a/docs/examples/apps/list-installation-scopes.md b/docs/examples/apps/list-installation-scopes.md new file mode 100644 index 00000000..4fa19c6e --- /dev/null +++ b/docs/examples/apps/list-installation-scopes.md @@ -0,0 +1,13 @@ +```javascript +import { Client, Apps } from 'react-native-appwrite'; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject(''); // Your project ID + +const apps = new Apps(client); + +const result = await apps.listInstallationScopes(); + +console.log(result); +``` diff --git a/docs/examples/apps/list-installations.md b/docs/examples/apps/list-installations.md new file mode 100644 index 00000000..9d58a9fc --- /dev/null +++ b/docs/examples/apps/list-installations.md @@ -0,0 +1,17 @@ +```javascript +import { Client, Apps } from 'react-native-appwrite'; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject(''); // Your project ID + +const apps = new Apps(client); + +const result = await apps.listInstallations({ + appId: '', + queries: [], // optional + total: false, // optional +}); + +console.log(result); +``` diff --git a/docs/examples/apps/list-keys.md b/docs/examples/apps/list-keys.md new file mode 100644 index 00000000..19d28553 --- /dev/null +++ b/docs/examples/apps/list-keys.md @@ -0,0 +1,17 @@ +```javascript +import { Client, Apps } from 'react-native-appwrite'; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject(''); // Your project ID + +const apps = new Apps(client); + +const result = await apps.listKeys({ + appId: '', + queries: [], // optional + total: false, // optional +}); + +console.log(result); +``` diff --git a/docs/examples/apps/list-o-auth-2-scopes.md b/docs/examples/apps/list-o-auth-2-scopes.md new file mode 100644 index 00000000..2c0ddac3 --- /dev/null +++ b/docs/examples/apps/list-o-auth-2-scopes.md @@ -0,0 +1,13 @@ +```javascript +import { Client, Apps } from 'react-native-appwrite'; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject(''); // Your project ID + +const apps = new Apps(client); + +const result = await apps.listOAuth2Scopes(); + +console.log(result); +``` diff --git a/docs/examples/apps/list-secrets.md b/docs/examples/apps/list-secrets.md new file mode 100644 index 00000000..f67bedbd --- /dev/null +++ b/docs/examples/apps/list-secrets.md @@ -0,0 +1,17 @@ +```javascript +import { Client, Apps } from 'react-native-appwrite'; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject(''); // Your project ID + +const apps = new Apps(client); + +const result = await apps.listSecrets({ + appId: '', + queries: [], // optional + total: false, // optional +}); + +console.log(result); +``` diff --git a/docs/examples/apps/list.md b/docs/examples/apps/list.md new file mode 100644 index 00000000..c0691b59 --- /dev/null +++ b/docs/examples/apps/list.md @@ -0,0 +1,16 @@ +```javascript +import { Client, Apps } from 'react-native-appwrite'; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject(''); // Your project ID + +const apps = new Apps(client); + +const result = await apps.list({ + queries: [], // optional + total: false, // optional +}); + +console.log(result); +``` diff --git a/docs/examples/apps/update-team.md b/docs/examples/apps/update-team.md new file mode 100644 index 00000000..6073a2f2 --- /dev/null +++ b/docs/examples/apps/update-team.md @@ -0,0 +1,16 @@ +```javascript +import { Client, Apps } from 'react-native-appwrite'; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject(''); // Your project ID + +const apps = new Apps(client); + +const result = await apps.updateTeam({ + appId: '', + teamId: '', +}); + +console.log(result); +``` diff --git a/docs/examples/apps/update.md b/docs/examples/apps/update.md new file mode 100644 index 00000000..38666ce7 --- /dev/null +++ b/docs/examples/apps/update.md @@ -0,0 +1,34 @@ +```javascript +import { Client, Apps } from 'react-native-appwrite'; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject(''); // Your project ID + +const apps = new Apps(client); + +const result = await apps.update({ + appId: '', + name: '', + description: '', // optional + clientUri: 'https://example.com', // optional + logoUri: 'https://example.com', // optional + privacyPolicyUrl: 'https://example.com', // optional + termsUrl: 'https://example.com', // optional + contacts: [], // optional + tagline: '', // optional + tags: [], // optional + images: [], // optional + supportUrl: 'https://example.com', // optional + dataDeletionUrl: 'https://example.com', // optional + enabled: false, // optional + redirectUris: [], // optional + postLogoutRedirectUris: [], // optional + type: 'public', // optional + deviceFlow: false, // optional + installationScopes: [], // optional + installationRedirectUrl: 'https://example.com', // optional +}); + +console.log(result); +``` diff --git a/docs/examples/avatars/delete-photo.md b/docs/examples/avatars/delete-photo.md new file mode 100644 index 00000000..a0120a62 --- /dev/null +++ b/docs/examples/avatars/delete-photo.md @@ -0,0 +1,13 @@ +```javascript +import { Client, Avatars } from 'react-native-appwrite'; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject(''); // Your project ID + +const avatars = new Avatars(client); + +const result = await avatars.deletePhoto(); + +console.log(result); +``` diff --git a/docs/examples/avatars/get-screenshot.md b/docs/examples/avatars/get-screenshot.md index 4e0402bc..c42066a7 100644 --- a/docs/examples/avatars/get-screenshot.md +++ b/docs/examples/avatars/get-screenshot.md @@ -17,8 +17,7 @@ const avatars = new Avatars(client); const result = avatars.getScreenshot({ url: 'https://example.com', headers: { - Authorization: 'Bearer token123', - 'X-Custom-Header': 'value', + 'Accept-Language': 'en-US,en;q=0.9', }, // optional viewportWidth: 1920, // optional viewportHeight: 1080, // optional diff --git a/docs/examples/avatars/update-photo.md b/docs/examples/avatars/update-photo.md new file mode 100644 index 00000000..05204200 --- /dev/null +++ b/docs/examples/avatars/update-photo.md @@ -0,0 +1,20 @@ +```javascript +import { Client, Avatars } from 'react-native-appwrite'; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject(''); // Your project ID + +const avatars = new Avatars(client); + +const result = await avatars.updatePhoto({ + file: { + name: 'image.png', + type: 'image/png', + size: 1024, + uri: 'file:///path/to/image.png', + }, +}); + +console.log(result); +``` diff --git a/docs/examples/databases/list-transactions.md b/docs/examples/databases/list-transactions.md index 051ea0f3..bc62da26 100644 --- a/docs/examples/databases/list-transactions.md +++ b/docs/examples/databases/list-transactions.md @@ -9,6 +9,7 @@ const databases = new Databases(client); const result = await databases.listTransactions({ queries: [], // optional + total: false, // optional }); console.log(result); diff --git a/docs/examples/documentsdb/create-documents.md b/docs/examples/documentsdb/create-documents.md index 891757c6..dc0cb531 100644 --- a/docs/examples/documentsdb/create-documents.md +++ b/docs/examples/documentsdb/create-documents.md @@ -10,7 +10,16 @@ const documentsDB = new DocumentsDB(client); const result = await documentsDB.createDocuments({ databaseId: '', collectionId: '', - documents: [], + documents: [ + { + $id: 'example1', + username: 'walter.obrien', + email: 'walter.obrien@example.com', + fullName: "Walter O'Brien", + age: 30, + isAdmin: false, + }, + ], transactionId: '', // optional }); diff --git a/docs/examples/documentsdb/list-transactions.md b/docs/examples/documentsdb/list-transactions.md index 24c8dbdb..d4a8c4ea 100644 --- a/docs/examples/documentsdb/list-transactions.md +++ b/docs/examples/documentsdb/list-transactions.md @@ -9,6 +9,7 @@ const documentsDB = new DocumentsDB(client); const result = await documentsDB.listTransactions({ queries: [], // optional + total: false, // optional }); console.log(result); diff --git a/docs/examples/graphql/mutation.md b/docs/examples/graphql/mutation.md index f5e673a2..89c0cd42 100644 --- a/docs/examples/graphql/mutation.md +++ b/docs/examples/graphql/mutation.md @@ -8,7 +8,9 @@ const client = new Client() const graphql = new Graphql(client); const result = await graphql.mutation({ - query: {}, + query: { + query: 'mutation { accountUpdateName(name: "Walter") { name } }', + }, }); console.log(result); diff --git a/docs/examples/graphql/query.md b/docs/examples/graphql/query.md index c9640ebe..872ebc0a 100644 --- a/docs/examples/graphql/query.md +++ b/docs/examples/graphql/query.md @@ -8,7 +8,9 @@ const client = new Client() const graphql = new Graphql(client); const result = await graphql.query({ - query: {}, + query: { + query: 'query { localeGet { ip } }', + }, }); console.log(result); diff --git a/docs/examples/locale/list-codes.md b/docs/examples/locale/list-codes.md index f96a7ee3..7157d5d8 100644 --- a/docs/examples/locale/list-codes.md +++ b/docs/examples/locale/list-codes.md @@ -7,7 +7,9 @@ const client = new Client() const locale = new Locale(client); -const result = await locale.listCodes(); +const result = await locale.listCodes({ + total: false, // optional +}); console.log(result); ``` diff --git a/docs/examples/locale/list-continents.md b/docs/examples/locale/list-continents.md index 8973cfd4..296b39d3 100644 --- a/docs/examples/locale/list-continents.md +++ b/docs/examples/locale/list-continents.md @@ -7,7 +7,9 @@ const client = new Client() const locale = new Locale(client); -const result = await locale.listContinents(); +const result = await locale.listContinents({ + total: false, // optional +}); console.log(result); ``` diff --git a/docs/examples/locale/list-countries-eu.md b/docs/examples/locale/list-countries-eu.md index f948ee69..4667a26f 100644 --- a/docs/examples/locale/list-countries-eu.md +++ b/docs/examples/locale/list-countries-eu.md @@ -7,7 +7,9 @@ const client = new Client() const locale = new Locale(client); -const result = await locale.listCountriesEU(); +const result = await locale.listCountriesEU({ + total: false, // optional +}); console.log(result); ``` diff --git a/docs/examples/locale/list-countries-phones.md b/docs/examples/locale/list-countries-phones.md index 7974d60e..4f719b3e 100644 --- a/docs/examples/locale/list-countries-phones.md +++ b/docs/examples/locale/list-countries-phones.md @@ -7,7 +7,9 @@ const client = new Client() const locale = new Locale(client); -const result = await locale.listCountriesPhones(); +const result = await locale.listCountriesPhones({ + total: false, // optional +}); console.log(result); ``` diff --git a/docs/examples/locale/list-countries.md b/docs/examples/locale/list-countries.md index b46bc615..ef9ff569 100644 --- a/docs/examples/locale/list-countries.md +++ b/docs/examples/locale/list-countries.md @@ -7,7 +7,9 @@ const client = new Client() const locale = new Locale(client); -const result = await locale.listCountries(); +const result = await locale.listCountries({ + total: false, // optional +}); console.log(result); ``` diff --git a/docs/examples/locale/list-currencies.md b/docs/examples/locale/list-currencies.md index 28498d9a..f3db19f3 100644 --- a/docs/examples/locale/list-currencies.md +++ b/docs/examples/locale/list-currencies.md @@ -7,7 +7,9 @@ const client = new Client() const locale = new Locale(client); -const result = await locale.listCurrencies(); +const result = await locale.listCurrencies({ + total: false, // optional +}); console.log(result); ``` diff --git a/docs/examples/locale/list-languages.md b/docs/examples/locale/list-languages.md index 835b04de..beb495b0 100644 --- a/docs/examples/locale/list-languages.md +++ b/docs/examples/locale/list-languages.md @@ -7,7 +7,9 @@ const client = new Client() const locale = new Locale(client); -const result = await locale.listLanguages(); +const result = await locale.listLanguages({ + total: false, // optional +}); console.log(result); ``` diff --git a/docs/examples/oauth2/approve.md b/docs/examples/oauth2/approve.md new file mode 100644 index 00000000..73ebd776 --- /dev/null +++ b/docs/examples/oauth2/approve.md @@ -0,0 +1,17 @@ +```javascript +import { Client, Oauth2 } from 'react-native-appwrite'; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject(''); // Your project ID + +const oauth2 = new Oauth2(client); + +const result = await oauth2.approve({ + grantId: '', + authorizationDetails: '', // optional + scope: '', // optional +}); + +console.log(result); +``` diff --git a/docs/examples/oauth2/authorize-post.md b/docs/examples/oauth2/authorize-post.md new file mode 100644 index 00000000..e27aa7e3 --- /dev/null +++ b/docs/examples/oauth2/authorize-post.md @@ -0,0 +1,28 @@ +```javascript +import { Client, Oauth2 } from 'react-native-appwrite'; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject(''); // Your project ID + +const oauth2 = new Oauth2(client); + +const result = await oauth2.authorizePost({ + clientId: '', // optional + redirectUri: 'https://example.com', // optional + responseType: '', // optional + scope: '', // optional + state: '', // optional + nonce: '', // optional + codeChallenge: '', // optional + codeChallengeMethod: 's256', // optional + prompt: '', // optional + maxAge: 0, // optional + authorizationDetails: '', // optional + resource: '', // optional + audience: '', // optional + requestUri: '', // optional +}); + +console.log(result); +``` diff --git a/docs/examples/oauth2/authorize.md b/docs/examples/oauth2/authorize.md new file mode 100644 index 00000000..039dc744 --- /dev/null +++ b/docs/examples/oauth2/authorize.md @@ -0,0 +1,28 @@ +```javascript +import { Client, Oauth2 } from 'react-native-appwrite'; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject(''); // Your project ID + +const oauth2 = new Oauth2(client); + +const result = await oauth2.authorize({ + clientId: '', // optional + redirectUri: 'https://example.com', // optional + responseType: '', // optional + scope: '', // optional + state: '', // optional + nonce: '', // optional + codeChallenge: '', // optional + codeChallengeMethod: 's256', // optional + prompt: '', // optional + maxAge: 0, // optional + authorizationDetails: '', // optional + resource: '', // optional + audience: '', // optional + requestUri: '', // optional +}); + +console.log(result); +``` diff --git a/docs/examples/oauth2/create-device-authorization.md b/docs/examples/oauth2/create-device-authorization.md new file mode 100644 index 00000000..c136011b --- /dev/null +++ b/docs/examples/oauth2/create-device-authorization.md @@ -0,0 +1,19 @@ +```javascript +import { Client, Oauth2 } from 'react-native-appwrite'; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject(''); // Your project ID + +const oauth2 = new Oauth2(client); + +const result = await oauth2.createDeviceAuthorization({ + clientId: '', // optional + scope: '', // optional + authorizationDetails: '', // optional + resource: '', // optional + audience: '', // optional +}); + +console.log(result); +``` diff --git a/docs/examples/oauth2/create-grant.md b/docs/examples/oauth2/create-grant.md new file mode 100644 index 00000000..2e5e3002 --- /dev/null +++ b/docs/examples/oauth2/create-grant.md @@ -0,0 +1,15 @@ +```javascript +import { Client, Oauth2 } from 'react-native-appwrite'; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject(''); // Your project ID + +const oauth2 = new Oauth2(client); + +const result = await oauth2.createGrant({ + userCode: '', +}); + +console.log(result); +``` diff --git a/docs/examples/oauth2/create-par.md b/docs/examples/oauth2/create-par.md new file mode 100644 index 00000000..5d9b5783 --- /dev/null +++ b/docs/examples/oauth2/create-par.md @@ -0,0 +1,27 @@ +```javascript +import { Client, Oauth2 } from 'react-native-appwrite'; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject(''); // Your project ID + +const oauth2 = new Oauth2(client); + +const result = await oauth2.createPAR({ + clientId: '', + redirectUri: 'https://example.com', + responseType: 'code', + scope: '', // optional + state: '', // optional + nonce: '', // optional + codeChallenge: '', // optional + codeChallengeMethod: 's256', // optional + prompt: '', // optional + maxAge: 0, // optional + authorizationDetails: '', // optional + resource: '', // optional + audience: '', // optional +}); + +console.log(result); +``` diff --git a/docs/examples/oauth2/create-token.md b/docs/examples/oauth2/create-token.md new file mode 100644 index 00000000..3be9ff4a --- /dev/null +++ b/docs/examples/oauth2/create-token.md @@ -0,0 +1,24 @@ +```javascript +import { Client, Oauth2 } from 'react-native-appwrite'; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject(''); // Your project ID + +const oauth2 = new Oauth2(client); + +const result = await oauth2.createToken({ + grantType: '', + code: '', // optional + refreshToken: '', // optional + deviceCode: '', // optional + clientId: '', // optional + clientSecret: '', // optional + codeVerifier: '', // optional + redirectUri: 'https://example.com', // optional + resource: '', // optional + audience: '', // optional +}); + +console.log(result); +``` diff --git a/docs/examples/oauth2/get-grant.md b/docs/examples/oauth2/get-grant.md new file mode 100644 index 00000000..e2851e77 --- /dev/null +++ b/docs/examples/oauth2/get-grant.md @@ -0,0 +1,15 @@ +```javascript +import { Client, Oauth2 } from 'react-native-appwrite'; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject(''); // Your project ID + +const oauth2 = new Oauth2(client); + +const result = await oauth2.getGrant({ + grantId: '', +}); + +console.log(result); +``` diff --git a/docs/examples/oauth2/list-organizations.md b/docs/examples/oauth2/list-organizations.md new file mode 100644 index 00000000..5305b17e --- /dev/null +++ b/docs/examples/oauth2/list-organizations.md @@ -0,0 +1,17 @@ +```javascript +import { Client, Oauth2 } from 'react-native-appwrite'; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject(''); // Your project ID + +const oauth2 = new Oauth2(client); + +const result = await oauth2.listOrganizations({ + limit: 1, // optional + offset: 0, // optional + search: '', // optional +}); + +console.log(result); +``` diff --git a/docs/examples/oauth2/list-projects.md b/docs/examples/oauth2/list-projects.md new file mode 100644 index 00000000..fe6f0c88 --- /dev/null +++ b/docs/examples/oauth2/list-projects.md @@ -0,0 +1,17 @@ +```javascript +import { Client, Oauth2 } from 'react-native-appwrite'; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject(''); // Your project ID + +const oauth2 = new Oauth2(client); + +const result = await oauth2.listProjects({ + limit: 1, // optional + offset: 0, // optional + search: '', // optional +}); + +console.log(result); +``` diff --git a/docs/examples/oauth2/logout-post.md b/docs/examples/oauth2/logout-post.md new file mode 100644 index 00000000..ad84a0c4 --- /dev/null +++ b/docs/examples/oauth2/logout-post.md @@ -0,0 +1,20 @@ +```javascript +import { Client, Oauth2 } from 'react-native-appwrite'; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject(''); // Your project ID + +const oauth2 = new Oauth2(client); + +const result = await oauth2.logoutPost({ + idTokenHint: '', // optional + logoutHint: '', // optional + clientId: '', // optional + postLogoutRedirectUri: 'https://example.com', // optional + state: '', // optional + uiLocales: '', // optional +}); + +console.log(result); +``` diff --git a/docs/examples/oauth2/logout.md b/docs/examples/oauth2/logout.md new file mode 100644 index 00000000..8d0e0e62 --- /dev/null +++ b/docs/examples/oauth2/logout.md @@ -0,0 +1,20 @@ +```javascript +import { Client, Oauth2 } from 'react-native-appwrite'; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject(''); // Your project ID + +const oauth2 = new Oauth2(client); + +const result = await oauth2.logout({ + idTokenHint: '', // optional + logoutHint: '', // optional + clientId: '', // optional + postLogoutRedirectUri: 'https://example.com', // optional + state: '', // optional + uiLocales: '', // optional +}); + +console.log(result); +``` diff --git a/docs/examples/oauth2/reject.md b/docs/examples/oauth2/reject.md new file mode 100644 index 00000000..3c6c5888 --- /dev/null +++ b/docs/examples/oauth2/reject.md @@ -0,0 +1,15 @@ +```javascript +import { Client, Oauth2 } from 'react-native-appwrite'; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject(''); // Your project ID + +const oauth2 = new Oauth2(client); + +const result = await oauth2.reject({ + grantId: '', +}); + +console.log(result); +``` diff --git a/docs/examples/oauth2/revoke.md b/docs/examples/oauth2/revoke.md new file mode 100644 index 00000000..5369493d --- /dev/null +++ b/docs/examples/oauth2/revoke.md @@ -0,0 +1,18 @@ +```javascript +import { Client, Oauth2 } from 'react-native-appwrite'; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject(''); // Your project ID + +const oauth2 = new Oauth2(client); + +const result = await oauth2.revoke({ + token: '', + tokenTypeHint: 'access_token', // optional + clientId: '', // optional + clientSecret: '', // optional +}); + +console.log(result); +``` diff --git a/docs/examples/tablesdb/list-transactions.md b/docs/examples/tablesdb/list-transactions.md index e28ff141..ef26c6bf 100644 --- a/docs/examples/tablesdb/list-transactions.md +++ b/docs/examples/tablesdb/list-transactions.md @@ -9,6 +9,7 @@ const tablesDB = new TablesDB(client); const result = await tablesDB.listTransactions({ queries: [], // optional + total: false, // optional }); console.log(result); diff --git a/docs/examples/teams/create-membership.md b/docs/examples/teams/create-membership.md index e7fe0d72..3c6ee849 100644 --- a/docs/examples/teams/create-membership.md +++ b/docs/examples/teams/create-membership.md @@ -9,7 +9,7 @@ const teams = new Teams(client); const result = await teams.createMembership({ teamId: '', - roles: [], + roles: ['editor'], email: 'email@example.com', // optional userId: '', // optional phone: '+12065550100', // optional diff --git a/docs/examples/teams/update-membership.md b/docs/examples/teams/update-membership.md index 92e59696..8b05702d 100644 --- a/docs/examples/teams/update-membership.md +++ b/docs/examples/teams/update-membership.md @@ -10,7 +10,7 @@ const teams = new Teams(client); const result = await teams.updateMembership({ teamId: '', membershipId: '', - roles: [], + roles: ['editor'], }); console.log(result); diff --git a/docs/examples/vectorsdb/create-operations.md b/docs/examples/vectorsdb/create-operations.md index 86be153f..11f4bb50 100644 --- a/docs/examples/vectorsdb/create-operations.md +++ b/docs/examples/vectorsdb/create-operations.md @@ -16,7 +16,10 @@ const result = await vectorsDB.createOperations({ collectionId: '', documentId: '', data: { - name: "Walter O'Brien", + embeddings: [0.12, -0.55, 0.88, 1.02], + metadata: { + name: 'First document', + }, }, }, ], // optional diff --git a/docs/examples/vectorsdb/list-transactions.md b/docs/examples/vectorsdb/list-transactions.md index 8a009a98..d29cc045 100644 --- a/docs/examples/vectorsdb/list-transactions.md +++ b/docs/examples/vectorsdb/list-transactions.md @@ -9,6 +9,7 @@ const vectorsDB = new VectorsDB(client); const result = await vectorsDB.listTransactions({ queries: [], // optional + total: false, // optional }); console.log(result); diff --git a/eslint.config.mjs b/eslint.config.mjs index b8d79f5d..206377bf 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -5,7 +5,13 @@ export default tseslint.config( eslint.configs.recommended, tseslint.configs.recommended, { - ignores: ['dist/', 'types/', 'docs/', 'rollup.config.mjs'], + ignores: [ + 'dist/', + 'types/', + 'docs/', + 'rollup.config.mjs', + 'react-native.config.js', + ], }, { rules: { diff --git a/ios/AppwriteCookies.m b/ios/AppwriteCookies.m new file mode 100644 index 00000000..7e1506a8 --- /dev/null +++ b/ios/AppwriteCookies.m @@ -0,0 +1,35 @@ +#import + +@interface AppwriteCookies : NSObject +@end + +@implementation AppwriteCookies + +RCT_EXPORT_MODULE() + ++ (BOOL)requiresMainQueueSetup +{ + return NO; +} + +RCT_EXPORT_METHOD(session:(NSString *)url + project:(NSString *)project + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) +{ + NSString *name = [@"a_session_" stringByAppendingString:project]; + NSURL *target = [NSURL URLWithString:url]; + if (target == nil) { + resolve([NSNull null]); + return; + } + for (NSHTTPCookie *cookie in [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookiesForURL:target]) { + if ([cookie.name isEqualToString:name]) { + resolve(cookie.value); + return; + } + } + resolve([NSNull null]); +} + +@end diff --git a/package-lock.json b/package-lock.json index 6458d5bb..a6215489 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,21 +1,27 @@ { "name": "react-native-appwrite", - "version": "1.1.0", + "version": "1.2.0-rc.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "react-native-appwrite", - "version": "1.1.0", + "version": "1.2.0-rc.0", "license": "BSD-3-Clause", "dependencies": { + "buffer": "6.0.3", "expo-file-system": "18.*.*", - "json-bigint": "1.0.0" + "json-bigint": "1.0.0", + "mqtt": "5.15.2", + "process": "0.11.10", + "readable-stream": "4.7.0" }, "devDependencies": { "@eslint/js": "10.0.1", "@rollup/plugin-typescript": "12.3.0", "@types/json-bigint": "1.0.4", + "@types/node": "24.9.2", + "@types/readable-stream": "4.0.24", "eslint": "10.10.0", "playwright": "1.59.0", "prettier": "3.9.6", @@ -30,14 +36,28 @@ }, "peerDependencies": { "expo": "*", - "react-native": ">=0.76.7 <1.0.0" + "expo-notifications": "*", + "react-native": ">=0.76.7 <1.0.0", + "react-native-background-actions": ">=4.0.0 <5.0.0", + "react-native-tcp-socket": ">=6.2.0 <7.0.0" + }, + "peerDependenciesMeta": { + "expo-notifications": { + "optional": true + }, + "react-native-background-actions": { + "optional": true + }, + "react-native-tcp-socket": { + "optional": true + } }, "overrides": { "glob": { ".": "^13.0.0", - "brace-expansion": "5.0.9" + "brace-expansion": "5.0.12" }, - "brace-expansion": "5.0.9", + "brace-expansion": "5.0.12", "rimraf": "^6.0.0", "@xmldom/xmldom": "^0.9.10", "uuid": "^14.0.0", @@ -434,9 +454,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.29.8", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", - "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "version": "7.29.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.9.tgz", + "integrity": "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==", "license": "MIT", "peer": true, "dependencies": { @@ -1047,9 +1067,9 @@ } }, "node_modules/@babel/plugin-transform-typescript": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.29.7.tgz", - "integrity": "sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw==", + "version": "7.29.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.29.9.tgz", + "integrity": "sha512-FFwIwzU+7SCOuxxV4YtJql6T9981ZVTm+FHO5GhVsRqCTdy0WwrEZh3l42ARpXruJUDGOptdduHW6Zr7pNPLNg==", "license": "MIT", "peer": true, "dependencies": { @@ -1108,7 +1128,6 @@ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", "license": "MIT", - "peer": true, "engines": { "node": ">=6.9.0" } @@ -1314,9 +1333,9 @@ } }, "node_modules/@expo/cli": { - "version": "57.0.25", - "resolved": "https://registry.npmjs.org/@expo/cli/-/cli-57.0.25.tgz", - "integrity": "sha512-G2FJfwClIPNPhH2sJlIFAkhQLYaKNGzSarIQUl6sVA8oGdrmBnhRvbtTzxDzjS6WzO2aDX8DY7y1aNbCFILodg==", + "version": "57.0.27", + "resolved": "https://registry.npmjs.org/@expo/cli/-/cli-57.0.27.tgz", + "integrity": "sha512-Jauk4chxmpVG5ElrMLTCwjFP20jbYU7PB4l3LYV8j+4e/3HU5jdl7qTQ4eVy1AaDUE9Ia2NCQ5A9kar2V1gaLg==", "license": "MIT", "peer": true, "dependencies": { @@ -1337,7 +1356,7 @@ "@expo/plist": "^0.8.1", "@expo/prebuild-config": "^57.0.16", "@expo/require-utils": "^57.0.5", - "@expo/router-server": "^57.0.10", + "@expo/router-server": "^57.0.11", "@expo/schema-utils": "^57.0.2", "@expo/spawn-async": "^1.8.0", "@expo/ws-tunnel": "^2.0.0", @@ -1779,18 +1798,18 @@ } }, "node_modules/@expo/router-server": { - "version": "57.0.10", - "resolved": "https://registry.npmjs.org/@expo/router-server/-/router-server-57.0.10.tgz", - "integrity": "sha512-8rGh6N5di7jzu7UC1SuIA1TWmVd+vBwOHwC7/1ww+FA6CcxcWyQlcPl39h2+Jpnul78CVzWpIjOcdKsZsp2zXw==", + "version": "57.0.11", + "resolved": "https://registry.npmjs.org/@expo/router-server/-/router-server-57.0.11.tgz", + "integrity": "sha512-0QxbqEkceb1WDeu5VqNdnTPVHlm3U183F7Y1t5LMohmGRAkHVL0PB4tCHBvEqKpLzOzHVObw3reGkJ1tX48h8Q==", "license": "MIT", "peer": true, "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { - "@expo/metro-runtime": "^57.0.15", + "@expo/metro-runtime": "^57.0.16", "expo": "*", - "expo-constants": "^57.0.18", + "expo-constants": "^57.0.19", "expo-font": "^57.0.4", "expo-router": "*", "expo-server": "^57.0.3", @@ -3157,13 +3176,30 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.20.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.3.tgz", - "integrity": "sha512-DZmzkmwHzXrLPAXPyKNDzlIwMMUZCVacoD25ywdy5YTKGbOx/2ld+Q38Im2zJ0vBuZP5Prd3VZutKZyXwkOS8A==", + "version": "24.9.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.2.tgz", + "integrity": "sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA==", "license": "MIT", - "peer": true, "dependencies": { - "undici-types": "~6.21.0" + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/readable-stream": { + "version": "4.0.24", + "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.24.tgz", + "integrity": "sha512-NRvUNC/JFGPJvqdAfEve8oginbM6V08u5NzLWpG8MwA2kTPOLnqk+wpwuPT+mp3aUsxyuT6m2gnrPuHYCruzEg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ws": { + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.2.tgz", + "integrity": "sha512-67MQl+fpWKVTT1NYdnmo3U4sc/xPo/zQBncVnI74qmQa0z/b+1g6iYqNmGCPbxO+zz2aklb08a0oHfegiVd0/w==", + "license": "MIT", + "dependencies": { + "@types/node": "*" } }, "node_modules/@types/yargs": { @@ -3213,9 +3249,9 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { - "version": "7.0.9", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.9.tgz", - "integrity": "sha512-brTTsvFRt5C1gGHtPst/281UjPD5t9fBqbgoMPlVWy11ZLTPfu7HxK4ZYqO9H7o/yC9rSTCI85EaQ4OoY12qYw==", + "version": "7.0.10", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.10.tgz", + "integrity": "sha512-HpbUakT7xp5miBUywCHf36ZEuAJNklBJDDsGpUIjMzOSmM8ELSfA9Sa/QDPeNeqeoN31u+UTCkL4klCOVvRm4Q==", "dev": true, "license": "MIT", "engines": { @@ -3430,6 +3466,18 @@ "node": ">=14.6" } }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -3652,9 +3700,9 @@ } }, "node_modules/babel-plugin-react-native-web": { - "version": "0.21.2", - "resolved": "https://registry.npmjs.org/babel-plugin-react-native-web/-/babel-plugin-react-native-web-0.21.2.tgz", - "integrity": "sha512-SPD0J6qjJn8231i0HZhlAGH6NORe+QvRSQM2mwQEzJ2Fb3E4ruWTiiicPlHjmeWShDXLcvoorOCXjeR7k/lyWA==", + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/babel-plugin-react-native-web/-/babel-plugin-react-native-web-0.21.3.tgz", + "integrity": "sha512-e4fG8NbyRvk5GwU/7G2U9WxzPYtpC4IQN28AyRDzKE0kvywgohq8vT2x0/xBfg8a0RfSkXLlHAtUAGlSy3VhAw==", "license": "MIT", "peer": true }, @@ -3679,9 +3727,9 @@ } }, "node_modules/babel-preset-expo": { - "version": "57.0.12", - "resolved": "https://registry.npmjs.org/babel-preset-expo/-/babel-preset-expo-57.0.12.tgz", - "integrity": "sha512-rrZqJPYbVevy7rqibJZQFd3AsleGslJ63vmB3Qs8b5poOO5AHaehUGbZHjb0HoOZO59d86BEifYbq0/ncoM2UQ==", + "version": "57.0.13", + "resolved": "https://registry.npmjs.org/babel-preset-expo/-/babel-preset-expo-57.0.13.tgz", + "integrity": "sha512-OOl3lqNQ4IagJfsNgtxuna0wpTWt1BswdyktGTOJ5EJCgdQUk/B2vaj/jsl03+kMyuIvpxKioVtbTzxh3mdvvQ==", "license": "MIT", "peer": true, "dependencies": { @@ -3731,7 +3779,7 @@ "peerDependencies": { "@babel/runtime": "^7.20.0", "expo": "*", - "expo-widgets": "^57.0.19", + "expo-widgets": "^57.0.21", "react-refresh": ">=0.14.0 <1.0.0" }, "peerDependenciesMeta": { @@ -3773,13 +3821,12 @@ "url": "https://feross.org/support" } ], - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.11.24", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.24.tgz", - "integrity": "sha512-hYrgxie335U08WqICoGqKRzV1HFXv6zdxwJE4ekCb80CM9a0SVVsN4QPwT67RraRo+9h8IATk6uxHJw7QSkdOg==", + "version": "2.11.26", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.26.tgz", + "integrity": "sha512-GLQdD3y6UF8iVuMJl5fHgE4jdn/ua7n+toKfLgNlg3BqQtOZjpy68T8Tup8/wGWZCDlm7KMg7tPb4MPn7oN0TQ==", "license": "Apache-2.0", "peer": true, "bin": { @@ -3808,6 +3855,18 @@ "node": "*" } }, + "node_modules/bl": { + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/bl/-/bl-6.1.6.tgz", + "integrity": "sha512-jLsPgN/YSvPUg9UX0Kd73CXpm2Psg9FxMeCSXnk3WBO3CMT10JMwijubhGfHCnFu6TPn1ei3b975dxv7K2pWVg==", + "license": "MIT", + "dependencies": { + "@types/readable-stream": "^4.0.0", + "buffer": "^6.0.3", + "inherits": "^2.0.4", + "readable-stream": "^4.2.0" + } + }, "node_modules/bplist-creator": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.1.0.tgz", @@ -3832,9 +3891,9 @@ } }, "node_modules/brace-expansion": { - "version": "5.0.9", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", - "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" @@ -3856,10 +3915,22 @@ "node": ">=8" } }, + "node_modules/broker-factory": { + "version": "3.1.15", + "resolved": "https://registry.npmjs.org/broker-factory/-/broker-factory-3.1.15.tgz", + "integrity": "sha512-ko+aWvgNuP49meGrdjUu7rC+Y+Wai3cCPxP3xWwHsHfehFjOh5ZQM2yC4gEB2UddeZ/YXhm0K1eG/L6fxym2Og==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "fast-unique-numbers": "^9.0.27", + "tslib": "^2.8.1", + "worker-factory": "^7.0.50" + } + }, "node_modules/browserslist": { - "version": "4.29.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.0.tgz", - "integrity": "sha512-3GSvyjvDI4Dur1Meg2BekJquu5uF+9R9a1+5M1Mde192eZoXbeXjzgOsgqPS2V8D5wrrip0gR5Hf/GhWQ9ZzaA==", + "version": "4.29.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.3.tgz", + "integrity": "sha512-1R4kiYKXGViqEN0CnoDrXc1StD9niAwu+j2dukWzrD4bJgsD4lDmEp0CRbc6E/vYJIfTHwPmwyaKtVSudICdPA==", "funding": [ { "type": "opencollective", @@ -3877,10 +3948,10 @@ "license": "MIT", "peer": true, "dependencies": { - "baseline-browser-mapping": "^2.11.23", - "caniuse-lite": "^1.0.30001810", - "electron-to-chromium": "^1.5.427", - "node-releases": "^2.0.55", + "baseline-browser-mapping": "^2.11.26", + "caniuse-lite": "^1.0.30001813", + "electron-to-chromium": "^1.5.439", + "node-releases": "^2.0.57", "update-browserslist-db": "^1.3.3" }, "bin": { @@ -3900,12 +3971,35 @@ "node-int64": "^0.4.0" } }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/bytes": { "version": "3.1.2", @@ -3945,9 +4039,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001810", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz", - "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==", + "version": "1.0.30001814", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001814.tgz", + "integrity": "sha512-/Uaf1lAzr59XcMpW0o96WoEfr+VXK2OX4U9AgFoiSHsVJ4HppnIFUjtYzsyDH2+tgANaQb2/oxYGwCPapN1FpA==", "funding": [ { "type": "opencollective", @@ -4125,6 +4219,12 @@ "node": ">= 10" } }, + "node_modules/commist": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/commist/-/commist-3.2.0.tgz", + "integrity": "sha512-4PIMoPniho+LqXmpS5d3NuGYncG6XWlkBSVGiWycL22dd42OYdUGil2CWuzklaJoNxyxUSpO4MKIBU94viWNAw==", + "license": "MIT" + }, "node_modules/compressible": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", @@ -4189,6 +4289,35 @@ "node": ">= 0.6" } }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/concat-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/connect": { "version": "3.7.0", "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", @@ -4377,9 +4506,9 @@ "peer": true }, "node_modules/electron-to-chromium": { - "version": "1.5.430", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.430.tgz", - "integrity": "sha512-e1QEj72Y4zd8RlNZVmoTg+iCOSVwpk05IOiiQwdrkwCSVlZfPthevErhE+nckGd2YbsXfp1SkisznhGVIXP2NQ==", + "version": "1.5.442", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.442.tgz", + "integrity": "sha512-najZYZ3+ZpjN1z3VOsrBiv5ej18vQgfV2lvEmxWzfmKrk4bdm3Owseyud4yGU6DfFn9pyunoAHDDyM4KmP78Ew==", "license": "ISC", "peer": true }, @@ -4620,15 +4749,33 @@ "node": ">= 0.6" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, "node_modules/expo": { - "version": "57.0.23", - "resolved": "https://registry.npmjs.org/expo/-/expo-57.0.23.tgz", - "integrity": "sha512-/aBLZFTD7+BNHWV/q5XbNRFqMhW/CrHsYeq8CcS/iGz6lJHYwEMm4XFcdoaeliuMdo6ZjNrsTHkBIdGB8VMHTg==", + "version": "57.0.26", + "resolved": "https://registry.npmjs.org/expo/-/expo-57.0.26.tgz", + "integrity": "sha512-U1rsFwcuZNnU61Z/YLnnVC/ZqP0wpxh/rCKUMff3ot/rhLpxFY1zxEqu4vyaexyvBAB1mwpl9lA/ltcHBoQGyg==", "license": "MIT", "peer": true, "dependencies": { "@babel/runtime": "^7.20.0", - "@expo/cli": "^57.0.25", + "@expo/cli": "^57.0.27", "@expo/config": "~57.0.9", "@expo/config-plugins": "~57.0.9", "@expo/devtools": "~57.0.1", @@ -4639,14 +4786,14 @@ "@expo/metro": "~56.0.2", "@expo/metro-config": "~57.0.12", "@ungap/structured-clone": "^1.3.0", - "babel-preset-expo": "~57.0.12", - "expo-asset": "~57.0.17", - "expo-constants": "~57.0.18", + "babel-preset-expo": "~57.0.13", + "expo-asset": "~57.0.18", + "expo-constants": "~57.0.20", "expo-file-system": "~57.0.7", "expo-font": "~57.0.4", "expo-keep-awake": "~57.0.2", "expo-modules-autolinking": "~57.0.13", - "expo-modules-core": "~57.0.18", + "expo-modules-core": "~57.0.20", "pretty-format": "^29.7.0", "react-refresh": "^0.14.2", "whatwg-url-minimum": "^0.1.2" @@ -4684,14 +4831,14 @@ } }, "node_modules/expo-asset": { - "version": "57.0.17", - "resolved": "https://registry.npmjs.org/expo-asset/-/expo-asset-57.0.17.tgz", - "integrity": "sha512-qSdWZBSfEg06o3GJMwcj36tw0vXkB6eYjqkQistWmyJkHxhUOKnsKJnev6OraXxLdoI+KOd1+qgp/ENx8gcqYA==", + "version": "57.0.18", + "resolved": "https://registry.npmjs.org/expo-asset/-/expo-asset-57.0.18.tgz", + "integrity": "sha512-zDIQkUr0NM2S7YRj98Zohf/OBuM1q0o4ctxLz1C1oe4/33lHNLjp+2v3qUtBWVwn0zKka8UGADmS8M+83Wi6pw==", "license": "MIT", "peer": true, "dependencies": { "@expo/image-utils": "^0.11.5", - "expo-constants": "~57.0.18" + "expo-constants": "~57.0.19" }, "peerDependencies": { "expo": "*", @@ -4700,9 +4847,9 @@ } }, "node_modules/expo-constants": { - "version": "57.0.18", - "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-57.0.18.tgz", - "integrity": "sha512-cLDwDOniPfuCZPHNY++cAY2ZkK97u5aobbBvilPK2LLmaWxR4ZpD31LWUIzUDLrwMPxLjmh1uURnbeYEf0Mivw==", + "version": "57.0.20", + "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-57.0.20.tgz", + "integrity": "sha512-WycPVvBgbluSa6yAh6DKnX646ZHrae0gV2ZLum5fioNkR+DiWyulq1+UfS++Jc6yfAh/l55NOirv6mt5cnQhSw==", "license": "MIT", "peer": true, "dependencies": { @@ -4766,14 +4913,14 @@ } }, "node_modules/expo-modules-core": { - "version": "57.0.18", - "resolved": "https://registry.npmjs.org/expo-modules-core/-/expo-modules-core-57.0.18.tgz", - "integrity": "sha512-ovB7C+GVQTlKAOIsOIHJVVXN9Mn0ljf2SYBS+afrRaGqq+HZEn1D3o+HReIVeH1NjCn/xDBb7I4HjldHs6luZA==", + "version": "57.0.20", + "resolved": "https://registry.npmjs.org/expo-modules-core/-/expo-modules-core-57.0.20.tgz", + "integrity": "sha512-vEL3aeih32yuD7sQHB3Y3LKXRT8mMgoVQWFIcHtX811Bl0StnNZUG2vT5je4fe+OWYVfkjb6gNqfMc04moREgA==", "license": "MIT", "peer": true, "dependencies": { "@expo/expo-modules-macros-plugin": "0.6.1", - "expo-modules-jsi": "~57.1.0", + "expo-modules-jsi": "~57.1.1", "invariant": "^2.2.4" }, "peerDependencies": { @@ -4788,9 +4935,9 @@ } }, "node_modules/expo-modules-jsi": { - "version": "57.1.0", - "resolved": "https://registry.npmjs.org/expo-modules-jsi/-/expo-modules-jsi-57.1.0.tgz", - "integrity": "sha512-a5ckeHfnbYfonhcHGkM0EU/a0Keh+/OufXy/HyFS+spr5Ib0N4Oh1ZsSYFQhp5xdOUFkVofKRhfH+VmcccIIpA==", + "version": "57.1.1", + "resolved": "https://registry.npmjs.org/expo-modules-jsi/-/expo-modules-jsi-57.1.1.tgz", + "integrity": "sha512-ssnwz3ZuIgCbZj5A3J6NlhQrXZojg+BjkeWrOWqVJhyMHlV/A/cRT7s986chKku97BFt6ucmDu6/BCzrV45Wcw==", "license": "MIT", "peer": true, "peerDependencies": { @@ -4846,6 +4993,19 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-unique-numbers": { + "version": "9.0.27", + "resolved": "https://registry.npmjs.org/fast-unique-numbers/-/fast-unique-numbers-9.0.27.tgz", + "integrity": "sha512-nDA9ADeINN8SA2u2wCtU+siWFTTDqQR37XvgPIDDmboWQeExz7X0mImxuaN+kJddliIqy2FpVRmnvRZ+j8i1/A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=18.2.0" + } + }, "node_modules/fb-dotslash": { "version": "0.5.8", "resolved": "https://registry.npmjs.org/fb-dotslash/-/fb-dotslash-0.5.8.tgz", @@ -5162,6 +5322,12 @@ "node": ">= 0.4" } }, + "node_modules/help-me": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz", + "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==", + "license": "MIT" + }, "node_modules/hermes-compiler": { "version": "250829098.0.17", "resolved": "https://registry.npmjs.org/hermes-compiler/-/hermes-compiler-250829098.0.17.tgz", @@ -5258,6 +5424,26 @@ "node": ">= 14" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -5281,8 +5467,7 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC", - "peer": true + "license": "ISC" }, "node_modules/invariant": { "version": "2.2.4", @@ -5294,13 +5479,22 @@ "loose-envify": "^1.0.0" } }, + "node_modules/ip-address": { + "version": "10.7.2", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.2.tgz", + "integrity": "sha512-7H/2gFSIitxc0hG3nOI1glS8QLo/EHBFFLk8vEUjXY/xu0AdL8jZ9U1IzO2PUm0d2D/ofQcAifb0g6OBkt8U7w==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/is-core-module": { - "version": "2.16.2", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", - "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "version": "2.17.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.17.0.tgz", + "integrity": "sha512-J/vG0zBCbIKOQFfufSwyXdMrsohyJIUNkrnmo6WZGzoM7tr/lsbfW5b2BvisL6zsyMzK9UxV9L6c7AoFbyXHOA==", "license": "MIT", "dependencies": { - "hasown": "^2.0.3" + "hasown": "^2.0.4" }, "engines": { "node": ">= 0.4" @@ -5485,6 +5679,16 @@ "license": "MIT", "peer": true }, + "node_modules/js-sdsl": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.3.0.tgz", + "integrity": "sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -6569,6 +6773,15 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/minipass": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", @@ -6592,6 +6805,55 @@ "node": ">=10" } }, + "node_modules/mqtt": { + "version": "5.15.2", + "resolved": "https://registry.npmjs.org/mqtt/-/mqtt-5.15.2.tgz", + "integrity": "sha512-VWZU2CSUY3U3oN0PSBRDE5SNsFi4zqqNeQ/uv3pZWqY3CrBXD/dhd0ZLjlsk5YnebGlrapi4lRVNJPUNQ5aZ3w==", + "license": "MIT", + "dependencies": { + "@types/readable-stream": "^4.0.21", + "@types/ws": "^8.18.1", + "commist": "^3.2.0", + "concat-stream": "^2.0.0", + "debug": "^4.4.1", + "help-me": "^5.0.0", + "lru-cache": "^10.4.3", + "minimist": "^1.2.8", + "mqtt-packet": "^9.0.2", + "number-allocator": "^1.0.14", + "readable-stream": "^4.7.0", + "rfdc": "^1.4.1", + "socks": "^2.8.6", + "split2": "^4.2.0", + "worker-timers": "^8.0.23", + "ws": "^8.18.3" + }, + "bin": { + "mqtt": "build/bin/mqtt.js", + "mqtt_pub": "build/bin/pub.js", + "mqtt_sub": "build/bin/sub.js" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/mqtt-packet": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/mqtt-packet/-/mqtt-packet-9.0.2.tgz", + "integrity": "sha512-MvIY0B8/qjq7bKxdN1eD+nrljoeaai+qjLJgfRn3TiMuz0pamsIWY2bFODPZMSNmabsLANXsLl4EMoWvlaTZWA==", + "license": "MIT", + "dependencies": { + "bl": "^6.0.8", + "debug": "^4.3.4", + "process-nextick-args": "^2.0.1" + } + }, + "node_modules/mqtt/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -6659,9 +6921,9 @@ "peer": true }, "node_modules/node-releases": { - "version": "2.0.55", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.55.tgz", - "integrity": "sha512-mIrE/Cw9y+9Au6dS5vDKDhQza9YvG6w+ZrS6X+ZzA7yFW/soAeaups4Qzn1bL6g5FVy8WtP79+0j82oPIbqRjQ==", + "version": "2.0.57", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.57.tgz", + "integrity": "sha512-kQK9LGGFiHtrWiNhZtA7Qbw17AQz+dmsEKODRIVTXA9+e5MS/2gZEBhYJt13GrAz5/IOZKddH/0Z3TP/Zgo+yw==", "license": "MIT", "peer": true, "engines": { @@ -6691,6 +6953,16 @@ "license": "MIT", "peer": true }, + "node_modules/number-allocator": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/number-allocator/-/number-allocator-1.0.14.tgz", + "integrity": "sha512-OrL44UTVAvkKdOdRQZIJpLkAdjXGTRda052sN4sO77bKEzYYqWKMBjQvrJFzqygI99gL6Z4u2xctPW1tB8ErvA==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.1", + "js-sdsl": "4.3.0" + } + }, "node_modules/ob1": { "version": "0.84.5", "resolved": "https://registry.npmjs.org/ob1/-/ob1-0.84.5.tgz", @@ -6976,9 +7248,9 @@ } }, "node_modules/path-scurry/node_modules/lru-cache": { - "version": "11.5.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", - "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "version": "11.5.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", + "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", "license": "BlueOak-1.0.0", "peer": true, "engines": { @@ -7161,6 +7433,21 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, "node_modules/progress": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", @@ -7435,6 +7722,22 @@ "node": ">=0.10.0" } }, + "node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, "node_modules/regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -7443,9 +7746,9 @@ "peer": true }, "node_modules/regenerate-unicode-properties": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz", - "integrity": "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==", + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.3.0.tgz", + "integrity": "sha512-9ns8odR8e9q3otKeHj3DE80jZYKg6zVeZKE+zQmRKXFoVUcuiTGeO6iusDhPRoIQQPVPRvH+SfBhrTud4eczuw==", "license": "MIT", "peer": true, "dependencies": { @@ -7463,14 +7766,14 @@ "peer": true }, "node_modules/regexpu-core": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz", - "integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==", + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.5.2.tgz", + "integrity": "sha512-zkmVH92DlzjSFc4xKGZp/2BXIFrDIdBQBC8gUXKCsMgKYb1bkCkxfSuoW1werDjaTEMTbULkHtKFHe1L+Abnaw==", "license": "MIT", "peer": true, "dependencies": { "regenerate": "^1.4.2", - "regenerate-unicode-properties": "^10.2.2", + "regenerate-unicode-properties": "^10.3.0", "regjsgen": "^0.8.0", "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", @@ -7488,9 +7791,9 @@ "peer": true }, "node_modules/regjsparser": { - "version": "0.13.2", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.2.tgz", - "integrity": "sha512-NgRBy2Nx/bE+9F27nVHnqcN5HjyLmecqsqx2PJHu3/IEtADD4WuxuXIVExD5PoSDFVrl78dOonfcOe5O+5nbzQ==", + "version": "0.13.3", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.3.tgz", + "integrity": "sha512-ycwFAS14Jw4mppvmK4GR/J6u3WpWpjkEApehuHtLc/8VpPNpDMbQ4WjqwplXifGeyKOzHSFLmSPqzksDQE2Sfg==", "license": "BSD-2-Clause", "peer": true, "dependencies": { @@ -7562,6 +7865,12 @@ "node": ">=4" } }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, "node_modules/rollup": { "version": "4.63.2", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.2.tgz", @@ -7626,8 +7935,7 @@ "url": "https://feross.org/support" } ], - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/sandbox-cli-detector": { "version": "0.2.0", @@ -7883,9 +8191,9 @@ } }, "node_modules/shell-quote": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.10.0.tgz", - "integrity": "sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.11.0.tgz", + "integrity": "sha512-JdxDPD0DBTyu08pq0kPC0xSNet/qsU07qT6IsX1AS8oO2ICNRY4ldNa8OAI6PuwAH8tG3lxEhbqmyp4Dw4036g==", "license": "MIT", "peer": true, "engines": { @@ -7944,6 +8252,30 @@ "node": ">=8.0.0" } }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.10", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.10.tgz", + "integrity": "sha512-e0VyvkVTwVYViNovRkZ9aodhxVlyoMn7eJhVUPxZ+eK9P/7CBkxvvsBOHqFPEH416726W8tLXXXjKwqgTErrCQ==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.1.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, "node_modules/source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -7985,6 +8317,15 @@ "node": ">=0.10.0" } }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/stackframe": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", @@ -8025,6 +8366,15 @@ "node": ">= 0.10.0" } }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -8242,7 +8592,6 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, "license": "0BSD" }, "node_modules/type-check": { @@ -8268,6 +8617,12 @@ "node": ">=8" } }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -8307,11 +8662,10 @@ } }, "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "license": "MIT", - "peer": true + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "license": "MIT" }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.1", @@ -8408,6 +8762,12 @@ "punycode": "^2.1.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -8518,6 +8878,53 @@ "node": ">=0.10.0" } }, + "node_modules/worker-factory": { + "version": "7.0.50", + "resolved": "https://registry.npmjs.org/worker-factory/-/worker-factory-7.0.50.tgz", + "integrity": "sha512-hhwc0G+sFwM4qBuhJIUBn2p1Jf8v/FwmLUANBf/Q+Lt2uI8mfIZQhXaZQACodQD4R7Zp6cn/6702bIvNn2puJQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "fast-unique-numbers": "^9.0.27", + "tslib": "^2.8.1" + } + }, + "node_modules/worker-timers": { + "version": "8.0.34", + "resolved": "https://registry.npmjs.org/worker-timers/-/worker-timers-8.0.34.tgz", + "integrity": "sha512-WXL+Dqsm0G6dnC66rQsvM3tPT2adhbqSirWUCZGglkALOr8ocS0KlpU0iuKbjflJOlu3ydZikMEVrYnHOM9suw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "tslib": "^2.8.1", + "worker-timers-broker": "^8.0.18", + "worker-timers-worker": "^9.0.15" + } + }, + "node_modules/worker-timers-broker": { + "version": "8.0.18", + "resolved": "https://registry.npmjs.org/worker-timers-broker/-/worker-timers-broker-8.0.18.tgz", + "integrity": "sha512-FrjzDVX1wKfZN0gRbCFqv8VHuTncG4sbI/WGEg4tSSQeIsnwqg4YBYWMAHYLJtUDEYYmiK65UKFrVMVk2irDSg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "broker-factory": "^3.1.15", + "fast-unique-numbers": "^9.0.27", + "tslib": "^2.8.1", + "worker-timers-worker": "^9.0.15" + } + }, + "node_modules/worker-timers-worker": { + "version": "9.0.15", + "resolved": "https://registry.npmjs.org/worker-timers-worker/-/worker-timers-worker-9.0.15.tgz", + "integrity": "sha512-KKUe7lZ/Aignr51H6hOUik8LwTnIgojH/1lwhli8A8qIEIyewogZTpNpMW5B6BF7nmwBOkUoTYgf1H3QShcjSA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "tslib": "^2.8.1", + "worker-factory": "^7.0.50" + } + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -8550,11 +8957,10 @@ } }, "node_modules/ws": { - "version": "8.21.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", - "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.22.0.tgz", + "integrity": "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==", "license": "MIT", - "peer": true, "engines": { "node": ">=10.0.0" }, diff --git a/package.json b/package.json index 5c05620f..90a6c48a 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": "1.1.0", + "version": "1.2.0-rc.0", "license": "BSD-3-Clause", "main": "dist/cjs/sdk.js", "exports": { @@ -16,7 +16,12 @@ "module": "dist/esm/sdk.js", "types": "types/index.d.ts", "files": [ + "android", + "!android/build", "dist", + "ios", + "*.podspec", + "react-native.config.js", "types" ], "repository": { @@ -41,6 +46,8 @@ "eslint": "10.10.0", "@rollup/plugin-typescript": "12.3.0", "@types/json-bigint": "1.0.4", + "@types/node": "24.9.2", + "@types/readable-stream": "4.0.24", "playwright": "1.59.0", "prettier": "3.9.6", "rollup": "4.63.2", @@ -50,19 +57,37 @@ "typescript-eslint": "8.70.0" }, "dependencies": { + "buffer": "6.0.3", "expo-file-system": "18.*.*", - "json-bigint": "1.0.0" + "json-bigint": "1.0.0", + "mqtt": "5.15.2", + "process": "0.11.10", + "readable-stream": "4.7.0" }, "peerDependencies": { "expo": "*", - "react-native": ">=0.76.7 <1.0.0" + "expo-notifications": "*", + "react-native": ">=0.76.7 <1.0.0", + "react-native-background-actions": ">=4.0.0 <5.0.0", + "react-native-tcp-socket": ">=6.2.0 <7.0.0" + }, + "peerDependenciesMeta": { + "expo-notifications": { + "optional": true + }, + "react-native-background-actions": { + "optional": true + }, + "react-native-tcp-socket": { + "optional": true + } }, "overrides": { "glob": { ".": "^13.0.0", - "brace-expansion": "5.0.9" + "brace-expansion": "5.0.12" }, - "brace-expansion": "5.0.9", + "brace-expansion": "5.0.12", "rimraf": "^6.0.0", "@xmldom/xmldom": "^0.9.10", "uuid": "^14.0.0", diff --git a/react-native-appwrite.podspec b/react-native-appwrite.podspec new file mode 100644 index 00000000..46bd90e3 --- /dev/null +++ b/react-native-appwrite.podspec @@ -0,0 +1,21 @@ +require 'json' + +package = JSON.parse(File.read(File.join(__dir__, 'package.json'))) + +Pod::Spec.new do |s| + s.name = package['name'] + s.version = package['version'] + s.summary = package['description'] + s.homepage = package['homepage'] + s.license = package['license'] + s.authors = 'Appwrite Team' + s.platforms = { :ios => '15.1' } + s.source = { :git => 'https://github.com/appwrite/sdk-for-react-native', :tag => s.version.to_s } + s.source_files = 'ios/**/*.{h,m,mm}' + + if respond_to?(:install_modules_dependencies, true) + install_modules_dependencies(s) + else + s.dependency 'React-Core' + end +end diff --git a/react-native.config.js b/react-native.config.js new file mode 100644 index 00000000..4bfcf04a --- /dev/null +++ b/react-native.config.js @@ -0,0 +1,12 @@ +// Autolinking: the native modules Push uses, found on iOS through the package's podspec. +module.exports = { + dependency: { + platforms: { + android: { + packageImportPath: + 'import io.appwrite.reactnative.AppwritePushPackage;', + packageInstance: 'new AppwritePushPackage()', + }, + }, + }, +}; diff --git a/src/client.ts b/src/client.ts index 2a6c814d..b6ae3afe 100644 --- a/src/client.ts +++ b/src/client.ts @@ -170,6 +170,14 @@ export type FilePart = { bytes: () => Promise; }; +function isFilePart(value: any): boolean { + return ( + typeof value.uri === 'string' && + typeof value.name === 'string' && + typeof value.type === 'string' + ); +} + class AppwriteException extends Error { code: number; response: string; @@ -193,6 +201,8 @@ class Client { config: { [key: string]: string } = { endpoint: 'https://cloud.appwrite.io/v1', endpointRealtime: '', + endpointPush: '', + pushClientId: '', platform: '', project: '', jwt: '', @@ -208,7 +218,7 @@ class Client { 'x-sdk-name': 'React Native', 'x-sdk-platform': 'client', 'x-sdk-language': 'reactnative', - 'x-sdk-version': '1.1.0', + 'x-sdk-version': '1.2.0-rc.0', 'X-Appwrite-Response-Format': '2.3.0', }; @@ -278,6 +288,45 @@ class Client { return this; } + /** + * Set Push Endpoint + * + * The MQTT broker URL the AppwritePush service connects to, e.g. + * `mqtt://host:1883` or `mqtts://host:8883`. + * + * @param {string} endpointPush + * + * @returns {this} + */ + setPushEndpoint(endpointPush: string): this { + if (!endpointPush || typeof endpointPush !== 'string') { + throw new AppwriteException('Endpoint must be a valid string'); + } + + this.config.endpointPush = endpointPush; + return this; + } + + /** + * Set Push Client Id + * + * A stable client id for the AppwritePush service. The broker keys its + * offline-replay cursor on this id, so pass a stable value to resume replay across + * app restarts. Defaults to a per-connection id when unset. + * + * @param {string} pushClientId + * + * @returns {this} + */ + setPushClientId(pushClientId: string): this { + if (!pushClientId || typeof pushClientId !== 'string') { + throw new AppwriteException('Client id must be a valid string'); + } + + this.config.pushClientId = pushClientId; + return this; + } + /** * Set platform * @@ -598,7 +647,14 @@ class Client { ); let session = this.config.session; - if (!session) { + // React Native has no localStorage; its WebSocket sends + // the session cookie instead. Only Expo web can have + // a cookie fallback here. + if ( + !session && + typeof window !== 'undefined' && + window.localStorage + ) { const cookie = JSONbig.parse( window.localStorage.getItem('cookieFallback') ?? '{}', @@ -810,6 +866,15 @@ class Client { params[key].forEach((value: any) => { formData.append(key + '[]', value); }); + } else if ( + params[key] !== null && + typeof params[key] === 'object' && + !isFilePart(params[key]) + ) { + formData.append( + key, + JSONbig.stringify(params[key]), + ); } else { formData.append(key, params[key]); } @@ -836,7 +901,9 @@ class Client { ); } - if ( + if (responseType === 'text' && response.status < 400) { + data = await response.text(); + } else if ( response.headers .get('content-type') ?.includes('application/json') diff --git a/src/enums/o-auth-provider.ts b/src/enums/o-auth-provider.ts index 92a95970..3dab5fb3 100644 --- a/src/enums/o-auth-provider.ts +++ b/src/enums/o-auth-provider.ts @@ -41,6 +41,7 @@ export enum OAuthProvider { Tradeshift = 'tradeshift', TradeshiftBox = 'tradeshiftBox', Twitch = 'twitch', + Webflow = 'webflow', Wordpress = 'wordpress', X = 'x', Yahoo = 'yahoo', diff --git a/src/index.ts b/src/index.ts index bef0780a..18a4e75a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,6 @@ export { Client, AppwriteException } from './client'; export { Account } from './services/account'; +export { Apps } from './services/apps'; export { Avatars } from './services/avatars'; export { Databases } from './services/databases'; export { DocumentsDB } from './services/documents-db'; @@ -7,6 +8,7 @@ export { Functions } from './services/functions'; export { Graphql } from './services/graphql'; export { Locale } from './services/locale'; export { Messaging } from './services/messaging'; +export { Oauth2 } from './services/oauth-2'; export { Organization } from './services/organization'; export { Presences } from './services/presences'; export { Storage } from './services/storage'; @@ -14,6 +16,13 @@ export { TablesDB } from './services/tables-db'; export { Teams } from './services/teams'; export { VectorsDB } from './services/vectors-db'; export { Realtime } from './services/realtime'; +export { Push } from './services/push'; +export type { + PushMessage, + PushSubscription, + SubscribeOptions, + MessageCallback, +} from './services/push'; export type { Models, Payload, @@ -26,6 +35,7 @@ export { Query } from './query'; export { Permission } from './permission'; export { Role } from './role'; export { ID } from './id'; +export { Topic, ResolvedTopic } from './topic'; export { Channel } from './channel'; export { Operator, Condition } from './operator'; export { AuthenticatorType } from './enums/authenticator-type'; diff --git a/src/lib/polyfills.ts b/src/lib/polyfills.ts new file mode 100644 index 00000000..08b3f64e --- /dev/null +++ b/src/lib/polyfills.ts @@ -0,0 +1,29 @@ +// Node-core globals that `mqtt` and `readable-stream` expect but React Native's +// runtime does not provide. Imported for side effects by the MQTT modules so the +// transport works without the app entry having to set anything up. Each shim is +// installed only when missing, so it never clobbers a real (or Node) global. +import { Buffer } from 'buffer'; +import process from 'process'; + +const g = globalThis as unknown as { + Buffer?: typeof Buffer; + process?: NodeJS.Process; +}; + +if (typeof g.Buffer === 'undefined') { + g.Buffer = Buffer; +} + +if (typeof g.process === 'undefined') { + g.process = process as unknown as NodeJS.Process; +} + +// readable-stream schedules work on process.nextTick; RN's process may lack it. +if (typeof g.process.nextTick !== 'function') { + g.process.nextTick = (( + cb: (...a: unknown[]) => void, + ...args: unknown[] + ) => { + setTimeout(() => cb(...args), 0); + }) as typeof process.nextTick; +} diff --git a/src/lib/tcp-stream.ts b/src/lib/tcp-stream.ts new file mode 100644 index 00000000..11d8f53c --- /dev/null +++ b/src/lib/tcp-stream.ts @@ -0,0 +1,125 @@ +import './polyfills'; + +import { Buffer } from 'buffer'; +import { Duplex } from 'readable-stream'; +import type TcpSocketType from 'react-native-tcp-socket'; +import type { IStream } from 'mqtt'; + +export interface TcpStreamOptions { + host: string; + port: number; + /** Wrap the connection in TLS (mqtts). */ + tls: boolean; + /** + * Skip certificate validation. A broker behind a certificate that does not + * chain to a public root either pins its root or turns validation off for + * testing. + */ + tlsInsecure: boolean; +} + +/** + * A Node-style Duplex over a raw TCP (or TLS) socket, for MQTT.js to run on. + * + * React Native has no `net` module, so MQTT.js cannot open a TCP connection on its + * own. `react-native-tcp-socket` provides the native socket; this adapts its + * EventEmitter-shaped `Socket` into the `readable-stream` Duplex MQTT.js pipes + * through its parser and writes packets to. + */ +export function createTcpStream({ + host, + port, + tls, + tlsInsecure, +}: TcpStreamOptions): IStream { + // `react-native-tcp-socket` is an optional peer. Requiring it inside try/catch marks it + // as an optional dependency for Metro, so merely importing the SDK (without native push) + // does not fail the bundler when the peer is absent; it is also only required lazily here, + // when a native push connection is actually opened. A missing peer surfaces as a clear + // error at that point rather than a Metro resolution failure on any SDK import. + /* eslint-disable @typescript-eslint/no-require-imports */ + let TcpSocket: typeof TcpSocketType; + try { + // react-native-tcp-socket is a CommonJS module (module.exports = { ... }) with no + // `.default`, so read that off first and fall back to the module for ESM interop. + const mod = require('react-native-tcp-socket'); + TcpSocket = mod.default ?? mod; + } catch { + throw new Error( + "Native push requires the optional peer 'react-native-tcp-socket'. Install it to use Push on React Native.", + ); + } + /* eslint-enable @typescript-eslint/no-require-imports */ + + let connected = false; + // Writes MQTT.js issues before the TCP handshake completes are held here and + // flushed on connect, so the CONNECT packet is never dropped on the opening socket. + const pending: { chunk: Buffer; cb: (err?: Error | null) => void }[] = []; + + // TLS must go through connectTLS(): plain Socket.connect() ignores a `tls` + // option entirely (it never performs a handshake), so sending MQTT bytes into + // the mqtts listener that way hangs until the broker's connack timeout. connectTLS + // returns a TLSSocket that does the handshake and emits 'secureConnect' when ready. + const socket = tls + ? TcpSocket.connectTLS( + // NB: react-native-tcp-socket disables certificate validation ONLY via + // `rejectUnauthorized: false` (its native BlindTrustManager). The documented + // `tlsCheckValidity` field is a dead type annotation the Android code ignores. + { + host, + port, + rejectUnauthorized: !tlsInsecure, + } as Parameters[0], + () => { + /* secureConnect — the handler below flushes the queue */ + }, + ) + : TcpSocket.createConnection({ host, port }, () => {}); + socket.setNoDelay(true); + + const duplex = new Duplex({ + read() { + socket.resume(); + }, + write(chunk: Buffer, _encoding, cb) { + if (!connected) { + pending.push({ chunk, cb }); + return; + } + socket.write(chunk, undefined, (err) => cb(err ?? null)); + }, + destroy(err, cb) { + socket.destroy(); + cb(err); + }, + }); + + const onConnect = () => { + if (connected) { + return; + } + connected = true; + for (const { chunk, cb } of pending) { + socket.write(chunk, undefined, (err) => cb(err ?? null)); + } + pending.length = 0; + }; + socket.on('connect', onConnect); + socket.on('secureConnect', onConnect); + + socket.on('data', (data) => { + // The native socket delivers Buffers by default (no encoding set); guard the + // string case just in case, decoding it as raw bytes. + const buf = + typeof data === 'string' ? Buffer.from(data, 'binary') : data; + if (!duplex.push(buf)) { + socket.pause(); + } + }); + socket.on('error', (err) => duplex.destroy(err)); + socket.on('close', () => { + duplex.push(null); + }); + + return duplex as unknown as IStream; +} diff --git a/src/models.ts b/src/models.ts index c632f386..09b8b39c 100644 --- a/src/models.ts +++ b/src/models.ts @@ -10,10 +10,12 @@ export namespace Models { */ export type RowList = { /** - * Total number of rows that matched your query. */ + * Total number of rows that matched your query. + */ total: number; /** - * List of rows. */ + * List of rows. + */ rows: Row[]; }; @@ -24,10 +26,12 @@ export namespace Models { Document extends Models.Document = Models.DefaultDocument, > = { /** - * Total number of documents that matched your query. */ + * Total number of documents that matched your query. + */ total: number; /** - * List of documents. */ + * List of documents. + */ documents: Document[]; }; @@ -36,10 +40,12 @@ export namespace Models { */ export type PresenceList = { /** - * Total number of presences that matched your query. */ + * Total number of presences that matched your query. + */ total: number; /** - * List of presences. */ + * List of presences. + */ presences: Presence[]; }; @@ -48,10 +54,12 @@ export namespace Models { */ export type SessionList = { /** - * Total number of sessions that matched your query. */ + * Total number of sessions that matched your query. + */ total: number; /** - * List of sessions. */ + * List of sessions. + */ sessions: Session[]; }; @@ -60,10 +68,12 @@ export namespace Models { */ export type IdentityList = { /** - * Total number of identities that matched your query. */ + * Total number of identities that matched your query. + */ total: number; /** - * List of identities. */ + * List of identities. + */ identities: Identity[]; }; @@ -72,10 +82,12 @@ export namespace Models { */ export type FileList = { /** - * Total number of files that matched your query. */ + * Total number of files that matched your query. + */ total: number; /** - * List of files. */ + * List of files. + */ files: File[]; }; @@ -86,10 +98,12 @@ export namespace Models { Preferences extends Models.Preferences = Models.DefaultPreferences, > = { /** - * Total number of teams that matched your query. */ + * Total number of teams that matched your query. + */ total: number; /** - * List of teams. */ + * List of teams. + */ teams: Team[]; }; @@ -98,10 +112,12 @@ export namespace Models { */ export type MembershipList = { /** - * Total number of memberships that matched your query. */ + * Total number of memberships that matched your query. + */ total: number; /** - * List of memberships. */ + * List of memberships. + */ memberships: Membership[]; }; @@ -110,10 +126,12 @@ export namespace Models { */ export type ExecutionList = { /** - * Total number of executions that matched your query. */ + * Total number of executions that matched your query. + */ total: number; /** - * List of executions. */ + * List of executions. + */ executions: Execution[]; }; @@ -122,10 +140,12 @@ export namespace Models { */ export type CountryList = { /** - * Total number of countries that matched your query. */ + * Total number of countries that matched your query. + */ total: number; /** - * List of countries. */ + * List of countries. + */ countries: Country[]; }; @@ -134,10 +154,12 @@ export namespace Models { */ export type ContinentList = { /** - * Total number of continents that matched your query. */ + * Total number of continents that matched your query. + */ total: number; /** - * List of continents. */ + * List of continents. + */ continents: Continent[]; }; @@ -146,10 +168,12 @@ export namespace Models { */ export type LanguageList = { /** - * Total number of languages that matched your query. */ + * Total number of languages that matched your query. + */ total: number; /** - * List of languages. */ + * List of languages. + */ languages: Language[]; }; @@ -158,10 +182,12 @@ export namespace Models { */ export type CurrencyList = { /** - * Total number of currencies that matched your query. */ + * Total number of currencies that matched your query. + */ total: number; /** - * List of currencies. */ + * List of currencies. + */ currencies: Currency[]; }; @@ -170,10 +196,12 @@ export namespace Models { */ export type PhoneList = { /** - * Total number of phones that matched your query. */ + * Total number of phones that matched your query. + */ total: number; /** - * List of phones. */ + * List of phones. + */ phones: Phone[]; }; @@ -182,10 +210,12 @@ export namespace Models { */ export type LocaleCodeList = { /** - * Total number of localeCodes that matched your query. */ + * Total number of localeCodes that matched your query. + */ total: number; /** - * List of localeCodes. */ + * List of localeCodes. + */ localeCodes: LocaleCode[]; }; @@ -194,10 +224,12 @@ export namespace Models { */ export type TransactionList = { /** - * Total number of transactions that matched your query. */ + * Total number of transactions that matched your query. + */ total: number; /** - * List of transactions. */ + * List of transactions. + */ transactions: Transaction[]; }; @@ -206,25 +238,32 @@ export namespace Models { */ export type Row = { /** - * Row ID. */ + * Row ID. + */ $id: string; /** - * Row sequence ID. */ + * Row sequence ID. + */ $sequence: string; /** - * Table ID. */ + * Table ID. + */ $tableId: string; /** - * Database ID. */ + * Database ID. + */ $databaseId: string; /** - * Row creation date in ISO 8601 format. */ + * Row creation date in ISO 8601 format. + */ $createdAt: string; /** - * Row update date in ISO 8601 format. */ + * Row update date in ISO 8601 format. + */ $updatedAt: string; /** - * Row permissions. [Learn more about permissions](https://appwrite.io/docs/permissions). */ + * Row permissions. [Learn more about permissions](https://appwrite.io/docs/permissions). + */ $permissions: string[]; }; @@ -238,25 +277,32 @@ export namespace Models { */ export type Document = { /** - * Document ID. */ + * Document ID. + */ $id: string; /** - * Document sequence ID. */ + * Document sequence ID. + */ $sequence: string; /** - * Collection ID. */ + * Collection ID. + */ $collectionId: string; /** - * Database ID. */ + * Database ID. + */ $databaseId: string; /** - * Document creation date in ISO 8601 format. */ + * Document creation date in ISO 8601 format. + */ $createdAt: string; /** - * Document update date in ISO 8601 format. */ + * Document update date in ISO 8601 format. + */ $updatedAt: string; /** - * Document permissions. [Learn more about permissions](https://appwrite.io/docs/permissions). */ + * Document permissions. [Learn more about permissions](https://appwrite.io/docs/permissions). + */ $permissions: string[]; }; @@ -270,31 +316,40 @@ export namespace Models { */ export type Presence = { /** - * Presence ID. */ + * Presence ID. + */ $id: string; /** - * Presence creation date in ISO 8601 format. */ + * Presence creation date in ISO 8601 format. + */ $createdAt: string; /** - * Presence update date in ISO 8601 format. */ + * Presence update date in ISO 8601 format. + */ $updatedAt: string; /** - * Presence permissions. [Learn more about permissions](https://appwrite.io/docs/permissions). */ + * Presence permissions. [Learn more about permissions](https://appwrite.io/docs/permissions). + */ $permissions: string[]; /** - * User ID. */ + * User ID. + */ userId: string; /** - * Presence status. */ + * Presence status. + */ status?: string; /** - * Presence source. */ + * Presence source. + */ source: string; /** - * Presence expiry date in ISO 8601 format. */ + * Presence expiry date in ISO 8601 format. + */ expiresAt?: string; /** - * Presence metadata. */ + * Presence metadata. + */ metadata?: object; }; @@ -305,85 +360,112 @@ export namespace Models { Preferences extends Models.Preferences = Models.DefaultPreferences, > = { /** - * User ID. */ + * User ID. + */ $id: string; /** - * User creation date in ISO 8601 format. */ + * User creation date in ISO 8601 format. + */ $createdAt: string; /** - * User update date in ISO 8601 format. */ + * User update date in ISO 8601 format. + */ $updatedAt: string; /** - * User name. */ + * User name. + */ name: string; /** - * Hashed user password. */ + * Hashed user password. + */ password?: string; /** - * Password hashing algorithm. */ + * Password hashing algorithm. + */ hash?: string; /** - * Password hashing algorithm configuration. */ + * Password hashing algorithm configuration. + */ hashOptions?: object; /** - * User registration date in ISO 8601 format. */ + * User registration date in ISO 8601 format. + */ registration: string; /** - * User status. Pass `true` for enabled and `false` for disabled. */ + * User status. Pass `true` for enabled and `false` for disabled. + */ status: boolean; /** - * Labels for the user. */ + * Labels for the user. + */ labels: string[]; /** - * Password update time in ISO 8601 format. */ + * Password update time in ISO 8601 format. + */ passwordUpdate: string; /** - * User email address. */ + * User email address. + */ email: string; /** - * User phone number in E.164 format. */ + * User phone number in E.164 format. + */ phone: string; /** - * Email verification status. */ + * Email verification status. + */ emailVerification: boolean; /** - * Canonical form of the user email address. */ + * Canonical form of the user email address. + */ emailCanonical?: string; /** - * Whether the user email is from a free email provider. */ + * Whether the user email is from a free email provider. + */ emailIsFree?: boolean; /** - * Whether the user email is from a disposable email provider. */ + * Whether the user email is from a disposable email provider. + */ emailIsDisposable?: boolean; /** - * Whether the user email is from a corporate domain. */ + * Whether the user email is from a corporate domain. + */ emailIsCorporate?: boolean; /** - * Whether the user email is in its canonical form. */ + * Whether the user email is in its canonical form. + */ emailIsCanonical?: boolean; /** - * Whether the password was found in a known data breach the last time it was checked. Null when the password has never been checked. */ + * Whether the password was found in a known data breach the last time it was checked. Null when the password has never been checked. + */ passwordPwned?: boolean; /** - * Phone verification status. */ + * Phone verification status. + */ phoneVerification: boolean; /** - * Multi factor authentication status. */ + * Multi factor authentication status. + */ mfa: boolean; /** - * User preferences as a key-value object */ + * User preferences as a key-value object + */ prefs: Preferences; /** - * A user-owned message receiver. A single user may have multiple e.g. emails, phones, and a browser. Each target is registered with a single provider. */ + * A user-owned message receiver. A single user may have multiple e.g. emails, phones, and a browser. Each target is registered with a single provider. + */ targets: Target[]; /** - * Most recent access date in ISO 8601 format. This attribute is only updated again after 24 hours. */ + * Most recent access date in ISO 8601 format. This attribute is only updated again after 24 hours. + */ accessedAt: string; /** - * Whether the user can impersonate other users. */ + * Whether the user can impersonate other users. + */ impersonator?: boolean; /** - * ID of the original actor performing the impersonation. Present only when the current request is impersonating another user. Internal audit logs attribute the action to this user, while the impersonated target is recorded only in internal audit payload data. */ + * ID of the original actor performing the impersonation. Present only when the current request is impersonating another user. Internal audit logs attribute the action to this user, while the impersonated target is recorded only in internal audit payload data. + */ impersonatorUserId?: string; }; @@ -392,7 +474,8 @@ export namespace Models { */ export type AlgoMd5 = { /** - * Algo type. */ + * Algo type. + */ type: string; }; @@ -401,7 +484,8 @@ export namespace Models { */ export type AlgoSha = { /** - * Algo type. */ + * Algo type. + */ type: string; }; @@ -410,7 +494,8 @@ export namespace Models { */ export type AlgoPhpass = { /** - * Algo type. */ + * Algo type. + */ type: string; }; @@ -419,7 +504,8 @@ export namespace Models { */ export type AlgoBcrypt = { /** - * Algo type. */ + * Algo type. + */ type: string; }; @@ -428,19 +514,24 @@ export namespace Models { */ export type AlgoScrypt = { /** - * Algo type. */ + * Algo type. + */ type: string; /** - * CPU complexity of computed hash. */ + * CPU complexity of computed hash. + */ costCpu: number; /** - * Memory complexity of computed hash. */ + * Memory complexity of computed hash. + */ costMemory: number; /** - * Parallelization of computed hash. */ + * Parallelization of computed hash. + */ costParallel: number; /** - * Length used to compute hash. */ + * Length used to compute hash. + */ length: number; }; @@ -449,16 +540,20 @@ export namespace Models { */ export type AlgoScryptModified = { /** - * Algo type. */ + * Algo type. + */ type: string; /** - * Salt used to compute hash. */ + * Salt used to compute hash. + */ salt: string; /** - * Separator used to compute hash. */ + * Separator used to compute hash. + */ saltSeparator: string; /** - * Key used to compute hash. */ + * Key used to compute hash. + */ signerKey: string; }; @@ -467,19 +562,127 @@ export namespace Models { */ export type AlgoArgon2 = { /** - * Algo type. */ + * Algo type. + */ type: string; /** - * Memory used to compute hash. */ + * Memory used to compute hash. + */ memoryCost: number; /** - * Amount of time consumed to compute hash */ + * Amount of time consumed to compute hash + */ timeCost: number; /** - * Number of threads used to compute hash. */ + * Number of threads used to compute hash. + */ threads: number; }; + /** + * Account + */ + export type Account< + Preferences extends Models.Preferences = Models.DefaultPreferences, + > = { + /** + * User ID. + */ + $id: string; + /** + * User creation date in ISO 8601 format. + */ + $createdAt: string; + /** + * User update date in ISO 8601 format. + */ + $updatedAt: string; + /** + * User name. + */ + name: string; + /** + * User registration date in ISO 8601 format. + */ + registration: string; + /** + * User status. Pass `true` for enabled and `false` for disabled. + */ + status: boolean; + /** + * Labels for the user. + */ + labels: string[]; + /** + * Password update time in ISO 8601 format. + */ + passwordUpdate: string; + /** + * User email address. + */ + email: string; + /** + * User phone number in E.164 format. + */ + phone: string; + /** + * Email verification status. + */ + emailVerification: boolean; + /** + * Canonical form of the user email address. + */ + emailCanonical?: string; + /** + * Whether the user email is from a free email provider. + */ + emailIsFree?: boolean; + /** + * Whether the user email is from a disposable email provider. + */ + emailIsDisposable?: boolean; + /** + * Whether the user email is from a corporate domain. + */ + emailIsCorporate?: boolean; + /** + * Whether the user email is in its canonical form. + */ + emailIsCanonical?: boolean; + /** + * Whether the password was found in a known data breach the last time it was checked. Null when the password has never been checked. + */ + passwordPwned?: boolean; + /** + * Phone verification status. + */ + phoneVerification: boolean; + /** + * Multi factor authentication status. + */ + mfa: boolean; + /** + * User preferences as a key-value object + */ + prefs: Preferences; + /** + * A user-owned message receiver. A single user may have multiple e.g. emails, phones, and a browser. Each target is registered with a single provider. + */ + targets: Target[]; + /** + * Most recent access date in ISO 8601 format. This attribute is only updated again after 24 hours. + */ + accessedAt: string; + /** + * Whether the user can impersonate other users. + */ + impersonator?: boolean; + /** + * ID of the original actor performing the impersonation. Present only when the current request is impersonating another user. Internal audit logs attribute the action to this user, while the impersonated target is recorded only in internal audit payload data. + */ + impersonatorUserId?: string; + }; + /** * Preferences */ @@ -495,91 +698,120 @@ export namespace Models { */ export type Session = { /** - * Session ID. */ + * Session ID. + */ $id: string; /** - * Session creation date in ISO 8601 format. */ + * Session creation date in ISO 8601 format. + */ $createdAt: string; /** - * Session update date in ISO 8601 format. */ + * Session update date in ISO 8601 format. + */ $updatedAt: string; /** - * User ID. */ + * User ID. + */ userId: string; /** - * Session expiration date in ISO 8601 format. */ + * Session expiration date in ISO 8601 format. + */ expire: string; /** - * Session Provider. */ + * Session Provider. + */ provider: string; /** - * Session Provider User ID. */ + * Session Provider User ID. + */ providerUid: string; /** - * Session Provider Access Token. */ + * Session Provider Access Token. + */ providerAccessToken: string; /** - * The date of when the access token expires in ISO 8601 format. */ + * The date of when the access token expires in ISO 8601 format. + */ providerAccessTokenExpiry: string; /** - * Session Provider Refresh Token. */ + * Session Provider Refresh Token. + */ providerRefreshToken: string; /** - * IP in use when the session was created. */ + * IP in use when the session was created. + */ ip: string; /** - * Operating system code name. View list of [available options](https://github.com/appwrite/appwrite/blob/master/docs/lists/os.json). */ + * Operating system code name. View list of [available options](https://github.com/appwrite/appwrite/blob/master/docs/lists/os.json). + */ osCode: string; /** - * Operating system name. */ + * Operating system name. + */ osName: string; /** - * Operating system version. */ + * Operating system version. + */ osVersion: string; /** - * Client type. */ + * Client type. + */ clientType: string; /** - * Client code name. View list of [available options](https://github.com/appwrite/appwrite/blob/master/docs/lists/clients.json). */ + * Client code name. View list of [available options](https://github.com/appwrite/appwrite/blob/master/docs/lists/clients.json). + */ clientCode: string; /** - * Client name. */ + * Client name. + */ clientName: string; /** - * Client version. */ + * Client version. + */ clientVersion: string; /** - * Client engine name. */ + * Client engine name. + */ clientEngine: string; /** - * Client engine name. */ + * Client engine name. + */ clientEngineVersion: string; /** - * Device name. */ + * Device name. + */ deviceName: string; /** - * Device brand name. */ + * Device brand name. + */ deviceBrand: string; /** - * Device model name. */ + * Device model name. + */ deviceModel: string; /** - * Country two-character ISO 3166-1 alpha code. */ + * Country two-character ISO 3166-1 alpha code. + */ countryCode: string; /** - * Country name. */ + * Country name. + */ countryName: string; /** - * Returns true if this the current user session. */ + * Returns true if this the current user session. + */ current: boolean; /** - * Returns a list of active session factors. */ + * Returns a list of active session factors. + */ factors: string[]; /** - * Secret used to authenticate the user. Only included if the request was made with an API key */ + * Secret used to authenticate the user. Only included if the request was made with an API key + */ secret: string; /** - * Most recent date in ISO 8601 format when the session successfully passed MFA challenge. */ + * Most recent date in ISO 8601 format when the session successfully passed MFA challenge. + */ mfaUpdatedAt: string; }; @@ -588,37 +820,48 @@ export namespace Models { */ export type Identity = { /** - * Identity ID. */ + * Identity ID. + */ $id: string; /** - * Identity creation date in ISO 8601 format. */ + * Identity creation date in ISO 8601 format. + */ $createdAt: string; /** - * Identity update date in ISO 8601 format. */ + * Identity update date in ISO 8601 format. + */ $updatedAt: string; /** - * User ID. */ + * User ID. + */ userId: string; /** - * Identity Provider. */ + * Identity Provider. + */ provider: string; /** - * ID of the User in the Identity Provider. */ + * ID of the User in the Identity Provider. + */ providerUid: string; /** - * Email of the User in the Identity Provider. */ + * Email of the User in the Identity Provider. + */ providerEmail: string; /** - * Identity Provider Access Token. */ + * Identity Provider Access Token. + */ providerAccessToken: string; /** - * The date of when the access token expires in ISO 8601 format. */ + * The date of when the access token expires in ISO 8601 format. + */ providerAccessTokenExpiry: string; /** - * Identity Provider Refresh Token. */ + * Identity Provider Refresh Token. + */ providerRefreshToken: string; /** - * Identity Provider ID token (JWT) from the most recent native sign-in. Empty for identities created through the browser OAuth2 flow. */ + * Identity Provider ID token (JWT) from the most recent native sign-in. Empty for identities created through the browser OAuth2 flow. + */ providerIdToken: string; }; @@ -627,22 +870,28 @@ export namespace Models { */ export type Token = { /** - * Token ID. */ + * Token ID. + */ $id: string; /** - * Token creation date in ISO 8601 format. */ + * Token creation date in ISO 8601 format. + */ $createdAt: string; /** - * User ID. */ + * User ID. + */ userId: string; /** - * Token secret key. This will return an empty string unless the response is returned using an API key or as part of a webhook payload. */ + * Token secret key. This will return an empty string unless the response is returned using an API key or as part of a webhook payload. + */ secret: string; /** - * Token expiration date in ISO 8601 format. */ + * Token expiration date in ISO 8601 format. + */ expire: string; /** - * Security phrase of a token. Empty if security phrase was not requested when creating a token. It includes randomly generated phrase which is also sent in the external resource such as email. */ + * Security phrase of a token. Empty if security phrase was not requested when creating a token. It includes randomly generated phrase which is also sent in the external resource such as email. + */ phrase: string; }; @@ -651,7 +900,8 @@ export namespace Models { */ export type Jwt = { /** - * JWT encoded string. */ + * JWT encoded string. + */ jwt: string; }; @@ -660,58 +910,76 @@ export namespace Models { */ export type Locale = { /** - * User IP address. */ + * User IP address. + */ ip: string; /** - * Country code in [ISO 3166-1](http://en.wikipedia.org/wiki/ISO_3166-1) two-character format */ + * Country code in [ISO 3166-1](http://en.wikipedia.org/wiki/ISO_3166-1) two-character format + */ countryCode: string; /** - * Country name. This field support localization. */ + * Country name. This field support localization. + */ country: string; /** - * Continent code. A two character continent code "AF" for Africa, "AN" for Antarctica, "AS" for Asia, "EU" for Europe, "NA" for North America, "OC" for Oceania, and "SA" for South America. */ + * Continent code. A two character continent code "AF" for Africa, "AN" for Antarctica, "AS" for Asia, "EU" for Europe, "NA" for North America, "OC" for Oceania, and "SA" for South America. + */ continentCode: string; /** - * Continent name. This field support localization. */ + * Continent name. This field support localization. + */ continent: string; /** - * True if country is part of the European Union. */ + * True if country is part of the European Union. + */ eu: boolean; /** - * Currency code in [ISO 4217-1](http://en.wikipedia.org/wiki/ISO_4217) three-character format */ + * Currency code in [ISO 4217-1](http://en.wikipedia.org/wiki/ISO_4217) three-character format + */ currency: string; /** - * City */ + * City + */ city?: string; /** - * Name of timezone */ + * Name of timezone + */ timeZone?: string; /** - * Postal code */ + * Postal code + */ postalCode?: string; /** - * Latitude */ + * Latitude + */ latitude?: number; /** - * Longitude */ + * Longitude + */ longitude?: number; /** - * Autonomous System Number (ASN) of the IP */ + * Autonomous System Number (ASN) of the IP + */ autonomousSystemNumber?: string; /** - * Organization that owns the ASN */ + * Organization that owns the ASN + */ autonomousSystemOrganization?: string; /** - * Internet service provider of the IP */ + * Internet service provider of the IP + */ isp?: string; /** - * Connection type of the IP (e.g. cable, cellular, corporate) */ + * Connection type of the IP (e.g. cable, cellular, corporate) + */ connectionType?: string; /** - * User type classification of the IP (e.g. residential, business, hosting) */ + * User type classification of the IP (e.g. residential, business, hosting) + */ connectionUsageType?: string; /** - * Registered organization of the IP */ + * Registered organization of the IP + */ connectionOrganization?: string; }; @@ -720,10 +988,12 @@ export namespace Models { */ export type LocaleCode = { /** - * Locale codes in [ISO 639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) */ + * Locale codes in [ISO 639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) + */ code: string; /** - * Locale name */ + * Locale name + */ name: string; }; @@ -732,52 +1002,68 @@ export namespace Models { */ export type File = { /** - * File ID. */ + * File ID. + */ $id: string; /** - * Bucket ID. */ + * Bucket ID. + */ bucketId: string; /** - * File creation date in ISO 8601 format. */ + * File creation date in ISO 8601 format. + */ $createdAt: string; /** - * File update date in ISO 8601 format. */ + * File update date in ISO 8601 format. + */ $updatedAt: string; /** - * File permissions. [Learn more about permissions](https://appwrite.io/docs/permissions). */ + * File permissions. [Learn more about permissions](https://appwrite.io/docs/permissions). + */ $permissions: string[]; /** - * File name. */ + * File name. + */ name: string; /** - * Virtual folder containing the file, with a trailing slash. Empty for the bucket root. */ + * Virtual folder containing the file, with a trailing slash. Empty for the bucket root. + */ folder: string; /** - * Full virtual path of the file: the folder followed by the file name. */ + * Full virtual path of the file: the folder followed by the file name. + */ key: string; /** - * File MD5 signature. */ + * File MD5 signature. + */ signature: string; /** - * File mime type. */ + * File mime type. + */ mimeType: string; /** - * File original size in bytes. */ + * File original size in bytes. + */ sizeOriginal: number; /** - * File actual stored size in bytes after compression and/or encryption. */ + * File actual stored size in bytes after compression and/or encryption. + */ sizeActual: number; /** - * Total number of chunks available */ + * Total number of chunks available + */ chunksTotal: number; /** - * Total number of chunks uploaded */ + * Total number of chunks uploaded + */ chunksUploaded: number; /** - * Whether file contents are encrypted at rest. */ + * Whether file contents are encrypted at rest. + */ encryption: boolean; /** - * Compression algorithm used for the file. Will be one of none, [gzip](https://en.wikipedia.org/wiki/Gzip), or [zstd](https://en.wikipedia.org/wiki/Zstd). */ + * Compression algorithm used for the file. Will be one of none, [gzip](https://en.wikipedia.org/wiki/Gzip), or [zstd](https://en.wikipedia.org/wiki/Zstd). + */ compression: string; }; @@ -788,22 +1074,28 @@ export namespace Models { Preferences extends Models.Preferences = Models.DefaultPreferences, > = { /** - * Team ID. */ + * Team ID. + */ $id: string; /** - * Team creation date in ISO 8601 format. */ + * Team creation date in ISO 8601 format. + */ $createdAt: string; /** - * Team update date in ISO 8601 format. */ + * Team update date in ISO 8601 format. + */ $updatedAt: string; /** - * Team name. */ + * Team name. + */ name: string; /** - * Total number of team members. */ + * Total number of team members. + */ total: number; /** - * Team preferences as a key-value object */ + * Team preferences as a key-value object + */ prefs: Preferences; }; @@ -812,49 +1104,64 @@ export namespace Models { */ export type Membership = { /** - * Membership ID. */ + * Membership ID. + */ $id: string; /** - * Membership creation date in ISO 8601 format. */ + * Membership creation date in ISO 8601 format. + */ $createdAt: string; /** - * Membership update date in ISO 8601 format. */ + * Membership update date in ISO 8601 format. + */ $updatedAt: string; /** - * User ID. */ + * User ID. + */ userId: string; /** - * User name. Hide this attribute by toggling membership privacy in the Console. */ + * User name. Hide this attribute by toggling membership privacy in the Console. + */ userName: string; /** - * User email address. Hide this attribute by toggling membership privacy in the Console. */ + * User email address. Hide this attribute by toggling membership privacy in the Console. + */ userEmail: string; /** - * User phone number. Hide this attribute by toggling membership privacy in the Console. */ + * User phone number. Hide this attribute by toggling membership privacy in the Console. + */ userPhone: string; /** - * Team ID. */ + * Team ID. + */ teamId: string; /** - * Team name. */ + * Team name. + */ teamName: string; /** - * Date, the user has been invited to join the team in ISO 8601 format. */ + * Date, the user has been invited to join the team in ISO 8601 format. + */ invited: string; /** - * Date, the user has accepted the invitation to join the team in ISO 8601 format. */ + * Date, the user has accepted the invitation to join the team in ISO 8601 format. + */ joined: string; /** - * User confirmation status, true if the user has joined the team or false otherwise. */ + * User confirmation status, true if the user has joined the team or false otherwise. + */ confirm: boolean; /** - * Multi factor authentication status, true if the user has MFA enabled or false otherwise. Hide this attribute by toggling membership privacy in the Console. */ + * Multi factor authentication status, true if the user has MFA enabled or false otherwise. Hide this attribute by toggling membership privacy in the Console. + */ mfa: boolean; /** - * Most recent access date in ISO 8601 format. Show this attribute by toggling membership privacy in the Console. */ + * Most recent access date in ISO 8601 format. Show this attribute by toggling membership privacy in the Console. + */ userAccessedAt: string; /** - * User list of roles */ + * User list of roles + */ roles: string[]; }; @@ -863,61 +1170,80 @@ export namespace Models { */ export type Execution = { /** - * Execution ID. */ + * Execution ID. + */ $id: string; /** - * Execution creation date in ISO 8601 format. */ + * Execution creation date in ISO 8601 format. + */ $createdAt: string; /** - * Execution update date in ISO 8601 format. */ + * Execution update date in ISO 8601 format. + */ $updatedAt: string; /** - * Execution roles. */ + * Execution roles. + */ $permissions: string[]; /** - * Function or site ID. */ + * Function or site ID. + */ resourceId: string; /** - * Execution resource type. */ + * Execution resource type. + */ resourceType: ExecutionResourceType; /** - * Deployment ID used to create the execution. */ + * Deployment ID used to create the execution. + */ deploymentId: string; /** - * The trigger that caused the resource to execute. Possible values can be: `http`, `schedule`, or `event`. */ + * The trigger that caused the resource to execute. Possible values can be: `http`, `schedule`, or `event`. + */ trigger: ExecutionTrigger; /** - * The status of the resource execution. Possible values can be: `waiting`, `processing`, `completed`, `failed`, or `scheduled`. */ + * The status of the resource execution. Possible values can be: `waiting`, `processing`, `completed`, `failed`, or `scheduled`. + */ status: ExecutionStatus; /** - * HTTP request method type. */ + * HTTP request method type. + */ requestMethod: string; /** - * HTTP request path and query. */ + * HTTP request path and query. + */ requestPath: string; /** - * HTTP request headers as a key-value object. This will return only whitelisted headers. All headers are returned if execution is created as synchronous. */ + * HTTP request headers as a key-value object. This will return only whitelisted headers. All headers are returned if execution is created as synchronous. + */ requestHeaders: Headers[]; /** - * HTTP response status code. */ + * HTTP response status code. + */ responseStatusCode: number; /** - * HTTP response body. This will return empty unless execution is created as synchronous. */ + * HTTP response body. This will return empty unless execution is created as synchronous. + */ responseBody: string; /** - * HTTP response headers as a key-value object. This will return only whitelisted headers. All headers are returned if execution is created as synchronous. */ + * HTTP response headers as a key-value object. This will return only whitelisted headers. All headers are returned if execution is created as synchronous. + */ responseHeaders: Headers[]; /** - * Resource logs. Includes the last 4,000 characters. This will return an empty string unless the response is returned using an API key or as part of a webhook payload. */ + * Resource logs. Includes the last 4,000 characters. This will return an empty string unless the response is returned using an API key or as part of a webhook payload. + */ logs: string; /** - * Resource errors. Includes the last 4,000 characters. This will return an empty string unless the response is returned using an API key or as part of a webhook payload. */ + * Resource errors. Includes the last 4,000 characters. This will return an empty string unless the response is returned using an API key or as part of a webhook payload. + */ errors: string; /** - * Total time the resource(function/site) took to respond, in seconds. */ + * Total time the resource(function/site) took to respond, in seconds. + */ duration: number; /** - * The scheduled time for execution. If left empty, execution will be queued immediately. */ + * The scheduled time for execution. If left empty, execution will be queued immediately. + */ scheduledAt?: string; }; @@ -926,10 +1252,12 @@ export namespace Models { */ export type Country = { /** - * Country name. */ + * Country name. + */ name: string; /** - * Country two-character ISO 3166-1 alpha code. */ + * Country two-character ISO 3166-1 alpha code. + */ code: string; }; @@ -938,10 +1266,12 @@ export namespace Models { */ export type Continent = { /** - * Continent name. */ + * Continent name. + */ name: string; /** - * Continent two letter code. */ + * Continent two letter code. + */ code: string; }; @@ -950,13 +1280,16 @@ export namespace Models { */ export type Language = { /** - * Language name. */ + * Language name. + */ name: string; /** - * Language two-character ISO 639-1 codes. */ + * Language two-character ISO 639-1 codes. + */ code: string; /** - * Language native name. */ + * Language native name. + */ nativeName: string; }; @@ -965,25 +1298,32 @@ export namespace Models { */ export type Currency = { /** - * Currency symbol. */ + * Currency symbol. + */ symbol: string; /** - * Currency name. */ + * Currency name. + */ name: string; /** - * Currency native symbol. */ + * Currency native symbol. + */ symbolNative: string; /** - * Number of decimal digits. */ + * Number of decimal digits. + */ decimalDigits: number; /** - * Currency digit rounding. */ + * Currency digit rounding. + */ rounding: number; /** - * Currency code in [ISO 4217-1](http://en.wikipedia.org/wiki/ISO_4217) three-character format. */ + * Currency code in [ISO 4217-1](http://en.wikipedia.org/wiki/ISO_4217) three-character format. + */ code: string; /** - * Currency plural name */ + * Currency plural name + */ namePlural: string; }; @@ -992,13 +1332,16 @@ export namespace Models { */ export type Phone = { /** - * Phone code. */ + * Phone code. + */ code: string; /** - * Country two-character ISO 3166-1 alpha code. */ + * Country two-character ISO 3166-1 alpha code. + */ countryCode: string; /** - * Country name. */ + * Country name. + */ countryName: string; }; @@ -1007,10 +1350,12 @@ export namespace Models { */ export type Headers = { /** - * Header name. */ + * Header name. + */ name: string; /** - * Header value. */ + * Header value. + */ value: string; }; @@ -1019,16 +1364,20 @@ export namespace Models { */ export type MfaChallenge = { /** - * Token ID. */ + * Token ID. + */ $id: string; /** - * Token creation date in ISO 8601 format. */ + * Token creation date in ISO 8601 format. + */ $createdAt: string; /** - * User ID. */ + * User ID. + */ userId: string; /** - * Token expiration date in ISO 8601 format. */ + * Token expiration date in ISO 8601 format. + */ expire: string; }; @@ -1037,7 +1386,8 @@ export namespace Models { */ export type MfaRecoveryCodes = { /** - * Recovery codes. */ + * Recovery codes. + */ recoveryCodes: string[]; }; @@ -1046,10 +1396,12 @@ export namespace Models { */ export type MfaType = { /** - * Secret token used for TOTP factor. */ + * Secret token used for TOTP factor. + */ secret: string; /** - * URI for authenticator apps. */ + * URI for authenticator apps. + */ uri: string; }; @@ -1058,19 +1410,24 @@ export namespace Models { */ export type MfaFactors = { /** - * Can TOTP be used for MFA challenge for this account. */ + * Can TOTP be used for MFA challenge for this account. + */ totp: boolean; /** - * Can phone (SMS) be used for MFA challenge for this account. */ + * Can phone (SMS) be used for MFA challenge for this account. + */ phone: boolean; /** - * Can email be used for MFA challenge for this account. */ + * Can email be used for MFA challenge for this account. + */ email: boolean; /** - * Can recovery code be used for MFA challenge for this account. */ + * Can recovery code be used for MFA challenge for this account. + */ recoveryCode: boolean; /** - * Can custom factor be used for MFA challenge for this account. */ + * Can custom factor be used for MFA challenge for this account. + */ custom: boolean; }; @@ -1079,22 +1436,28 @@ export namespace Models { */ export type Transaction = { /** - * Transaction ID. */ + * Transaction ID. + */ $id: string; /** - * Transaction creation time in ISO 8601 format. */ + * Transaction creation time in ISO 8601 format. + */ $createdAt: string; /** - * Transaction update date in ISO 8601 format. */ + * Transaction update date in ISO 8601 format. + */ $updatedAt: string; /** - * Current status of the transaction. One of: pending, committing, committed, rolled_back, failed. */ + * Current status of the transaction. One of: pending, committing, committed, rolled_back, failed. + */ status: string; /** - * Number of operations in the transaction. */ + * Number of operations in the transaction. + */ operations: number; /** - * Expiration time in ISO 8601 format. */ + * Expiration time in ISO 8601 format. + */ expiresAt: string; }; @@ -1103,31 +1466,40 @@ export namespace Models { */ export type Subscriber = { /** - * Subscriber ID. */ + * Subscriber ID. + */ $id: string; /** - * Subscriber creation time in ISO 8601 format. */ + * Subscriber creation time in ISO 8601 format. + */ $createdAt: string; /** - * Subscriber update date in ISO 8601 format. */ + * Subscriber update date in ISO 8601 format. + */ $updatedAt: string; /** - * Target ID. */ + * Target ID. + */ targetId: string; /** - * Target. */ + * Target. + */ target: Target; /** - * Topic ID. */ + * Topic ID. + */ userId: string; /** - * User Name. */ + * User Name. + */ userName: string; /** - * Topic ID. */ + * Topic ID. + */ topicId: string; /** - * The target provider type. Can be one of the following: `email`, `sms` or `push`. */ + * The target provider type. Can be one of the following: `email`, `sms` or `push`. + */ providerType: string; }; @@ -1136,103 +1508,560 @@ export namespace Models { */ export type Target = { /** - * Target ID. */ + * Target ID. + */ $id: string; /** - * Target creation time in ISO 8601 format. */ + * Target creation time in ISO 8601 format. + */ $createdAt: string; /** - * Target update date in ISO 8601 format. */ + * Target update date in ISO 8601 format. + */ $updatedAt: string; /** - * Target Name. */ + * Target Name. + */ name: string; /** - * User ID. */ + * User ID. + */ userId: string; /** - * Provider ID. */ + * Provider ID. + */ providerId?: string; /** - * The target provider type. Can be one of the following: `email`, `sms` or `push`. */ + * The target provider type. Can be one of the following: `email`, `sms` or `push`. + */ providerType: string; /** - * The target identifier. */ + * The target identifier. + */ identifier: string; /** - * Is the target expired. */ + * Is the target expired. + */ expired: boolean; }; + /** + * App + */ + export type App = { + /** + * App ID. + */ + $id: string; + /** + * App creation time in ISO 8601 format. + */ + $createdAt: string; + /** + * App update date in ISO 8601 format. + */ + $updatedAt: string; + /** + * Application name. + */ + name: string; + /** + * Application description shown to users during OAuth2 consent. + */ + description: string; + /** + * Application homepage URL shown to users during OAuth2 consent. + */ + clientUri: string; + /** + * Application logo URL shown to users during OAuth2 consent. + */ + logoUri: string; + /** + * Application privacy policy URL shown to users during OAuth2 consent. + */ + privacyPolicyUrl: string; + /** + * Application terms of service URL shown to users during OAuth2 consent. + */ + termsUrl: string; + /** + * Application support or security contact emails. + */ + contacts: string[]; + /** + * Application tagline shown to users during OAuth2 consent. + */ + tagline: string; + /** + * Application tags shown to users during OAuth2 consent. + */ + tags: string[]; + /** + * Application labels. Read-only for clients; only a server SDK using a project API key can update them. + */ + labels: string[]; + /** + * Application image URLs shown to users during OAuth2 consent. + */ + images: string[]; + /** + * Application support URL shown to users during OAuth2 consent. + */ + supportUrl: string; + /** + * Application data deletion URL shown to users during OAuth2 consent. + */ + dataDeletionUrl: string; + /** + * List of authorized redirect URIs. These URIs can be used to redirect users after they authenticate. + */ + redirectUris: string[]; + /** + * List of authorized post-logout redirect URIs for OpenID Connect RP-Initiated Logout. The logout endpoint only redirects users to URIs in this list after ending their session. + */ + postLogoutRedirectUris: string[]; + /** + * Whether the app is enabled or not. + */ + enabled: boolean; + /** + * OAuth2 client type. `public` for SPAs, mobile, and native apps that cannot keep a client secret (PKCE required); `confidential` for server-side clients that authenticate with a client secret. + */ + type: string; + /** + * Whether this client may use the OAuth2 Device Authorization Grant (RFC 8628). + */ + deviceFlow: boolean; + /** + * ID of team that owns the application, if owned by team. Otherwise, user ID will be used. + */ + teamId: string; + /** + * ID of user who owns the application, if owned by user. Otherwise, team ID will be used. + */ + userId: string; + /** + * Scopes the application requests when installed on a team. Organization-level and project-level scopes only. + */ + installationScopes: string[]; + /** + * URL users are redirected to after creating or updating an installation of this application. Empty for no redirect. + */ + installationRedirectUrl: string; + /** + * List of application secrets. + */ + secrets: AppSecret[]; + }; + + /** + * AppSecret + */ + export type AppSecret = { + /** + * Secret ID. + */ + $id: string; + /** + * Secret creation time in ISO 8601 format. + */ + $createdAt: string; + /** + * Secret update time in ISO 8601 format. + */ + $updatedAt: string; + /** + * Application ID this secret belongs to. + */ + appId: string; + /** + * Always empty. The application client secret is returned only once, in the response of the createSecret method. + */ + secret: string; + /** + * Last few characters of the client secret, used to help identify it. + */ + hint: string; + /** + * ID of the user who created the secret. + */ + createdById: string; + /** + * Name of the user who created the secret. + */ + createdByName: string; + /** + * Time the secret was last used for authentication in ISO 8601 format. Null if never used. + */ + lastAccessedAt?: string; + }; + + /** + * AppSecretPlaintext + */ + export type AppSecretPlaintext = { + /** + * Secret ID. + */ + $id: string; + /** + * Secret creation time in ISO 8601 format. + */ + $createdAt: string; + /** + * Secret update time in ISO 8601 format. + */ + $updatedAt: string; + /** + * Application ID this secret belongs to. + */ + appId: string; + /** + * Application client secret. Returned only when the secret is created; subsequent reads always return an empty value. + */ + secret: string; + /** + * Last few characters of the client secret, used to help identify it. + */ + hint: string; + /** + * ID of the user who created the secret. + */ + createdById: string; + /** + * Name of the user who created the secret. + */ + createdByName: string; + /** + * Time the secret was last used for authentication in ISO 8601 format. Null if never used. + */ + lastAccessedAt?: string; + }; + + /** + * AppScope + */ + export type AppScope = { + /** + * Scope value as requested by apps. + */ + value: string; + /** + * Human-readable description of what the scope grants. + */ + description: string; + /** + * What the scope grants access to. One of `account`, `project`, or `organization`. Only `project` and `organization` scopes are installable. + */ + type: string; + /** + * Scope category, used to group scopes on consent and installation screens. + */ + category: string; + /** + * Whether the scope is deprecated. Deprecated scopes can still be requested but should not be offered for new grants. + */ + deprecated: boolean; + }; + /** * AppInstallation */ export type AppInstallation = { /** - * Installation ID. */ + * Installation ID. + */ $id: string; /** - * Installation creation time in ISO 8601 format. */ + * Installation creation time in ISO 8601 format. + */ $createdAt: string; /** - * Installation update time in ISO 8601 format. */ + * Installation update time in ISO 8601 format. + */ $updatedAt: string; /** - * ID of the installed application. */ + * ID of the installed application. + */ appId: string; /** - * ID of the team the application is installed on. */ + * ID of the team the application is installed on. + */ teamId: string; /** - * Scopes granted to the application. Snapshot of the application's installation scopes taken when the installation was created or last updated. */ + * Scopes granted to the application. Snapshot of the application's installation scopes taken when the installation was created or last updated. + */ scopes: string[]; /** - * Authorization details granted to the application. Rich authorization request (RFC 9396) style entries; the Appwrite Console stores authorized project IDs here. */ + * Authorization details granted to the application. Rich authorization request (RFC 9396) style entries; the Appwrite Console stores authorized project IDs here. + */ authorizationDetails: Record[]; /** - * ID of the user who created the installation. */ + * ID of the user who created the installation. + */ createdById: string; /** - * Name of the user who created the installation. */ + * Name of the user who created the installation. + */ createdByName: string; /** - * Time an access token was last issued for the installation in ISO 8601 format. Null if never used. */ + * Time an access token was last issued for the installation in ISO 8601 format. Null if never used. + */ lastAccessedAt?: string; }; + /** + * AppKey + */ + export type AppKey = { + /** + * App key ID. + */ + $id: string; + /** + * App key creation time in ISO 8601 format. + */ + $createdAt: string; + /** + * App key update time in ISO 8601 format. + */ + $updatedAt: string; + /** + * Application ID this app key belongs to. + */ + appId: string; + /** + * App key secret. + */ + secret: string; + /** + * Last few characters of the app key secret, used to help identify it. + */ + hint: string; + /** + * ID of the user who created the app key. + */ + createdById: string; + /** + * Name of the user who created the app key. + */ + createdByName: string; + /** + * Time the app key was last used for authentication in ISO 8601 format. Null if never used. + */ + lastAccessedAt?: string; + }; + + /** + * OAuth2 Authorize + */ + export type Oauth2Authorize = { + /** + * OAuth2 grant ID. Set when the user must give explicit consent; pass it to the approve or reject endpoint. Empty when a redirect URL is returned instead. + */ + grantId: string; + /** + * URL the end user should be redirected to when the flow can complete without consent. Empty when consent is still required. + */ + redirectUrl: string; + }; + + /** + * OAuth2 Approve + */ + export type Oauth2Approve = { + /** + * URL the end user should be redirected to after the grant is approved, carrying the authorization `code` and/or `id_token` along with the original `state`. + */ + redirectUrl: string; + }; + + /** + * OAuth2 Reject + */ + export type Oauth2Reject = { + /** + * URL the end user should be redirected to after the grant is rejected, carrying an `access_denied` error. + */ + redirectUrl: string; + }; + + /** + * OAuth2 Grant + */ + export type Oauth2Grant = { + /** + * Grant ID. + */ + $id: string; + /** + * Grant creation time in ISO 8601 format. + */ + $createdAt: string; + /** + * Grant update date in ISO 8601 format. + */ + $updatedAt: string; + /** + * ID of the user the grant belongs to. + */ + userId: string; + /** + * ID of the OAuth2 client (app) the grant was requested for. + */ + appId: string; + /** + * Requested OAuth2 scopes the user is being asked to consent to. + */ + scopes: string[]; + /** + * Requested RFC 8707 resource indicators the user is being asked to consent to. + */ + resources: string[]; + /** + * Requested authorization_details the user is being asked to consent to, as a JSON string. Each entry has a `type` plus project-defined fields. + */ + authorizationDetails: string; + /** + * OIDC prompt directive the consent screen should honor. Space-separated list of: login, consent, select_account. + */ + prompt: string; + /** + * Redirect URI the user will be sent to after the flow completes. + */ + redirectUri: string; + /** + * Unix timestamp of when the user last authenticated. + */ + authTime: number; + /** + * Grant expiration time in ISO 8601 format. + */ + expire: string; + }; + + /** + * OAuth2 Device Authorization + */ + export type Oauth2DeviceAuthorization = { + /** + * Device verification code used by the client to poll the token endpoint. + */ + device_code: string; + /** + * Short code the end user enters on the verification page. + */ + user_code: string; + /** + * URL where the end user enters the user code. + */ + verification_uri: string; + /** + * Verification URL with the user code prefilled as a query parameter. + */ + verification_uri_complete: string; + /** + * Lifetime of the device code and user code in seconds. + */ + expires_in: number; + /** + * Minimum polling interval for the token endpoint in seconds. + */ + interval: number; + }; + + /** + * OAuth2 PAR + */ + export type Oauth2PAR = { + /** + * Authorization request handle to pass to the authorize endpoint. + */ + request_uri: string; + /** + * Lifetime of the authorization request handle in seconds. + */ + expires_in: number; + }; + + /** + * OAuth2 Token + */ + export type Oauth2Token = { + /** + * OAuth2 access token. + */ + access_token: string; + /** + * OAuth2 token type. + */ + token_type: string; + /** + * Access token lifetime in seconds. + */ + expires_in: number; + /** + * OAuth2 refresh token. + */ + refresh_token: string; + /** + * Space-separated scopes granted to the access token. + */ + scope: string; + /** + * Granted RFC 9396 authorization details as a JSON string. + */ + authorization_details?: string; + /** + * OpenID Connect ID token. Returned when the `openid` scope is granted. + */ + id_token?: string; + }; + /** * OAuth2 Consent */ export type Oauth2Consent = { /** - * Consent ID. */ + * Consent ID. + */ $id: string; /** - * Consent creation time in ISO 8601 format. */ + * Consent creation time in ISO 8601 format. + */ $createdAt: string; /** - * Consent update date in ISO 8601 format. */ + * Consent update date in ISO 8601 format. + */ $updatedAt: string; /** - * ID of the user the consent belongs to. */ + * ID of the user the consent belongs to. + */ userId: string; /** - * ID of the registered app the consent was given to. Empty for URL-form (CIMD) clients. */ + * ID of the registered app the consent was given to. Empty for URL-form (CIMD) clients. + */ appId: string; /** - * Client ID metadata document URL of the client the consent was given to. Empty for registered apps. */ + * Client ID metadata document URL of the client the consent was given to. Empty for registered apps. + */ cimdUrl: string; /** - * OAuth2 scopes the user consented to. */ + * OAuth2 scopes the user consented to. + */ scopes: string[]; /** - * RFC 8707 resource indicators the user consented to. */ + * RFC 8707 resource indicators the user consented to. + */ resources: string[]; /** - * Authorization details the user consented to, as a JSON string. Each entry has a `type` plus project-defined fields. */ + * Authorization details the user consented to, as a JSON string. Each entry has a `type` plus project-defined fields. + */ authorizationDetails: string; /** - * Consent expiration time in ISO 8601 format. Empty when the consent has no token-bound expiry yet. */ + * Consent expiration time in ISO 8601 format. Empty when the consent has no token-bound expiry yet. + */ expire: string; }; @@ -1241,49 +2070,118 @@ export namespace Models { */ export type Oauth2ConsentToken = { /** - * Token family ID. */ + * Token family ID. + */ $id: string; /** - * Token creation time in ISO 8601 format. */ + * Token creation time in ISO 8601 format. + */ $createdAt: string; /** - * Token update date in ISO 8601 format. Refreshing the token family updates this. */ + * Token update date in ISO 8601 format. Refreshing the token family updates this. + */ $updatedAt: string; /** - * ID of the consent the token family was issued under. */ + * ID of the consent the token family was issued under. + */ consentId: string; /** - * ID of the user the token family belongs to. */ + * ID of the user the token family belongs to. + */ userId: string; /** - * ID of the registered app the token family was issued to. Empty for URL-form (CIMD) clients. */ + * ID of the registered app the token family was issued to. Empty for URL-form (CIMD) clients. + */ appId: string; /** - * Client ID metadata document URL of the client the token family was issued to. Empty for registered apps. */ + * Client ID metadata document URL of the client the token family was issued to. Empty for registered apps. + */ cimdUrl: string; /** - * OAuth2 scopes granted on the token family. */ + * OAuth2 scopes granted on the token family. + */ scopes: string[]; /** - * RFC 8707 resource indicators granted on the token family. */ + * RFC 8707 resource indicators granted on the token family. + */ resources: string[]; /** - * Authorization details granted on the token family, as a JSON string. Each entry has a `type` plus project-defined fields. */ + * Authorization details granted on the token family, as a JSON string. Each entry has a `type` plus project-defined fields. + */ authorizationDetails: string; /** - * Expiration time of the current access token of this family in ISO 8601 format. */ + * Expiration time of the current access token of this family in ISO 8601 format. + */ expire: string; }; + /** + * OAuth2 Project + */ + export type Oauth2Project = { + /** + * Project ID. + */ + $id: string; + /** + * Region ID the project is deployed in. + */ + region: string; + /** + * API endpoint of the region the project is deployed in. Empty when the region has no public hostname configured. + */ + endpoint: string; + }; + + /** + * OAuth2 Organization + */ + export type Oauth2Organization = { + /** + * Organization ID. + */ + $id: string; + }; + + /** + * OAuth2 accessible projects list + */ + export type Oauth2ProjectList = { + /** + * Total number of projects that matched your query. + */ + total: number; + /** + * List of projects. + */ + projects: Oauth2Project[]; + }; + + /** + * OAuth2 accessible organizations list + */ + export type Oauth2OrganizationList = { + /** + * Total number of organizations that matched your query. + */ + total: number; + /** + * List of organizations. + */ + organizations: Oauth2Organization[]; + }; + /** * OAuth2 consents list */ export type Oauth2ConsentList = { /** - * Total number of consents that matched your query. */ + * Total number of consents that matched your query. + */ total: number; /** - * List of consents. */ + * List of consents. + */ consents: Oauth2Consent[]; }; @@ -1292,22 +2190,82 @@ export namespace Models { */ export type Oauth2ConsentTokenList = { /** - * Total number of tokens that matched your query. */ + * Total number of tokens that matched your query. + */ total: number; /** - * List of tokens. */ + * List of tokens. + */ tokens: Oauth2ConsentToken[]; }; + /** + * Apps list + */ + export type AppsList = { + /** + * Total number of apps that matched your query. + */ + total: number; + /** + * List of apps. + */ + apps: App[]; + }; + + /** + * App secrets list + */ + export type AppSecretList = { + /** + * Total number of secrets that matched your query. + */ + total: number; + /** + * List of secrets. + */ + secrets: AppSecret[]; + }; + + /** + * App scopes list + */ + export type AppScopeList = { + /** + * Total number of scopes that matched your query. + */ + total: number; + /** + * List of scopes. + */ + scopes: AppScope[]; + }; + /** * App installations list */ export type AppInstallationList = { /** - * Total number of installations that matched your query. */ + * Total number of installations that matched your query. + */ total: number; /** - * List of installations. */ + * List of installations. + */ installations: AppInstallation[]; }; + + /** + * App keys list + */ + export type AppKeyList = { + /** + * Total number of keys that matched your query. + */ + total: number; + /** + * List of keys. + */ + keys: AppKey[]; + }; } diff --git a/src/react-native-shim.d.ts b/src/react-native-shim.d.ts index fd9952a7..da1709b2 100644 --- a/src/react-native-shim.d.ts +++ b/src/react-native-shim.d.ts @@ -2,4 +2,105 @@ declare module 'react-native' { export const Platform: { readonly OS: string; }; + + // The slice of the native module API the Push service uses on Android. + export const NativeModules: Record | undefined; + + export interface EmitterSubscription { + remove(): void; + } + + export class NativeEventEmitter { + constructor(nativeModule?: unknown); + addListener( + eventType: string, + listener: (event: T) => void, + ): EmitterSubscription; + } +} + +// `react-native-tcp-socket` is an optional peer dependency (a native module), +// so it is not installed when type-checking the SDK in isolation. Declare just +// the slice of its API the MQTT transport uses so `tsc` resolves the import. +declare module 'react-native-tcp-socket' { + import type { Buffer } from 'buffer'; + + export interface Socket { + setNoDelay(noDelay?: boolean): Socket; + write( + buffer: string | Buffer, + encoding?: unknown, + cb?: (err?: Error) => void, + ): boolean; + destroy(error?: Error): void; + resume(): Socket; + pause(): Socket; + on(event: 'data', listener: (data: Buffer | string) => void): Socket; + on(event: 'error', listener: (error: Error) => void): Socket; + on( + event: 'connect' | 'secureConnect' | 'close', + listener: () => void, + ): Socket; + } + + export interface TcpSocketConnectOpts { + host?: string; + port: number; + } + + export interface TLSSocketOptions extends TcpSocketConnectOpts { + rejectUnauthorized?: boolean; + } + + interface TcpSocketStatic { + createConnection( + options: TcpSocketConnectOpts, + callback?: () => void, + ): Socket; + connectTLS(options: TLSSocketOptions, callback?: () => void): Socket; + } + + const TcpSockets: TcpSocketStatic; + export default TcpSockets; +} + +// Optional peer dependencies used only by Push.setForeground() (Android foreground +// service + local notifications). Declared so `tsc` resolves the dynamic imports even +// when the app has not installed them; at runtime the imports are guarded. +declare module 'react-native-background-actions' { + interface BackgroundTaskOptions { + taskName: string; + taskTitle: string; + taskDesc: string; + // Required by react-native-background-actions for its ongoing notification; + // omitting it makes start() throw "Task icon not found". + taskIcon: { name: string; type: string }; + foregroundServiceType?: string[]; + } + + interface BackgroundActions { + start( + task: (args?: unknown) => Promise, + options: BackgroundTaskOptions, + ): Promise; + stop(): Promise; + isRunning(): boolean; + } + + const backgroundActions: BackgroundActions; + export default backgroundActions; +} + +declare module 'expo-notifications' { + export const AndroidImportance: { readonly HIGH: number }; + + export function requestPermissionsAsync(): Promise<{ granted: boolean }>; + export function setNotificationChannelAsync( + channelId: string, + channel: { name: string; importance: number }, + ): Promise; + export function scheduleNotificationAsync(request: { + content: { title: string; body: string }; + trigger: null; + }): Promise; } diff --git a/src/services/account.ts b/src/services/account.ts index 245ca446..fb3d545b 100644 --- a/src/services/account.ts +++ b/src/services/account.ts @@ -22,7 +22,7 @@ export class Account extends Service { Preferences extends Models.Preferences = Models.DefaultPreferences, >(): Promise> { const apiPath = '/account'; - const payload: Payload = {}; + const apiPayload: Payload = {}; const uri = new URL(this.client.config.endpoint + apiPath); return this.client.call( @@ -32,7 +32,7 @@ export class Account extends Service { 'X-Appwrite-Project': this.client.config.project, accept: 'application/json', }, - payload, + apiPayload, ); } @@ -124,22 +124,22 @@ export class Account extends Service { } const apiPath = '/account'; - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof userId !== 'undefined') { - payload['userId'] = userId; + apiPayload['userId'] = userId; } if (typeof email !== 'undefined') { - payload['email'] = email; + apiPayload['email'] = email; } if (typeof password !== 'undefined') { - payload['password'] = password; + apiPayload['password'] = password; } if (typeof name !== 'undefined') { - payload['name'] = name; + apiPayload['name'] = name; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -151,7 +151,7 @@ export class Account extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -207,14 +207,14 @@ export class Account extends Service { const total = params.total; const apiPath = '/account/consents'; - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof queries !== 'undefined') { - payload['queries'] = queries; + apiPayload['queries'] = queries; } if (typeof total !== 'undefined') { - payload['total'] = total; + apiPayload['total'] = total; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -225,7 +225,7 @@ export class Account extends Service { 'X-Appwrite-Project': this.client.config.project, accept: 'application/json', }, - payload, + apiPayload, ); } @@ -275,7 +275,7 @@ export class Account extends Service { '{consentId}', encodeURIComponent(String(consentId)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; const uri = new URL(this.client.config.endpoint + apiPath); return this.client.call( @@ -285,7 +285,7 @@ export class Account extends Service { 'X-Appwrite-Project': this.client.config.project, accept: 'application/json', }, - payload, + apiPayload, ); } @@ -333,7 +333,7 @@ export class Account extends Service { '{consentId}', encodeURIComponent(String(consentId)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; const uri = new URL(this.client.config.endpoint + apiPath); return this.client.call( @@ -344,7 +344,7 @@ export class Account extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -416,14 +416,14 @@ export class Account extends Service { '{consentId}', encodeURIComponent(String(consentId)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof queries !== 'undefined') { - payload['queries'] = queries; + apiPayload['queries'] = queries; } if (typeof total !== 'undefined') { - payload['total'] = total; + apiPayload['total'] = total; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -434,7 +434,7 @@ export class Account extends Service { 'X-Appwrite-Project': this.client.config.project, accept: 'application/json', }, - payload, + apiPayload, ); } @@ -503,7 +503,7 @@ export class Account extends Service { const apiPath = '/account/consents/{consentId}/tokens/{tokenId}' .replace('{consentId}', encodeURIComponent(String(consentId))) .replace('{tokenId}', encodeURIComponent(String(tokenId))); - const payload: Payload = {}; + const apiPayload: Payload = {}; const uri = new URL(this.client.config.endpoint + apiPath); return this.client.call( @@ -513,7 +513,7 @@ export class Account extends Service { 'X-Appwrite-Project': this.client.config.project, accept: 'application/json', }, - payload, + apiPayload, ); } @@ -579,7 +579,7 @@ export class Account extends Service { const apiPath = '/account/consents/{consentId}/tokens/{tokenId}' .replace('{consentId}', encodeURIComponent(String(consentId))) .replace('{tokenId}', encodeURIComponent(String(tokenId))); - const payload: Payload = {}; + const apiPayload: Payload = {}; const uri = new URL(this.client.config.endpoint + apiPath); return this.client.call( @@ -590,7 +590,7 @@ export class Account extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -662,14 +662,14 @@ export class Account extends Service { } const apiPath = '/account/email'; - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof email !== 'undefined') { - payload['email'] = email; + apiPayload['email'] = email; } if (typeof password !== 'undefined') { - payload['password'] = password; + apiPayload['password'] = password; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -681,7 +681,7 @@ export class Account extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -737,14 +737,14 @@ export class Account extends Service { const total = params.total; const apiPath = '/account/identities'; - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof queries !== 'undefined') { - payload['queries'] = queries; + apiPayload['queries'] = queries; } if (typeof total !== 'undefined') { - payload['total'] = total; + apiPayload['total'] = total; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -755,7 +755,7 @@ export class Account extends Service { 'X-Appwrite-Project': this.client.config.project, accept: 'application/json', }, - payload, + apiPayload, ); } @@ -805,7 +805,7 @@ export class Account extends Service { '{identityId}', encodeURIComponent(String(identityId)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; const uri = new URL(this.client.config.endpoint + apiPath); return this.client.call( @@ -816,7 +816,7 @@ export class Account extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -858,10 +858,10 @@ export class Account extends Service { const duration = params.duration; const apiPath = '/account/jwts'; - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof duration !== 'undefined') { - payload['duration'] = duration; + apiPayload['duration'] = duration; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -873,7 +873,7 @@ export class Account extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -924,10 +924,10 @@ export class Account extends Service { } const apiPath = '/account/mfa'; - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof mfa !== 'undefined') { - payload['mfa'] = mfa; + apiPayload['mfa'] = mfa; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -939,7 +939,7 @@ export class Account extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -991,7 +991,7 @@ export class Account extends Service { '{type}', encodeURIComponent(String(type)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; const uri = new URL(this.client.config.endpoint + apiPath); return this.client.call( @@ -1002,7 +1002,7 @@ export class Account extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -1053,7 +1053,7 @@ export class Account extends Service { '{type}', encodeURIComponent(String(type)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; const uri = new URL(this.client.config.endpoint + apiPath); return this.client.call( @@ -1064,7 +1064,7 @@ export class Account extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -1136,10 +1136,10 @@ export class Account extends Service { '{type}', encodeURIComponent(String(type)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof otp !== 'undefined') { - payload['otp'] = otp; + apiPayload['otp'] = otp; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -1151,7 +1151,7 @@ export class Account extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -1222,10 +1222,10 @@ export class Account extends Service { '{type}', encodeURIComponent(String(type)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof otp !== 'undefined') { - payload['otp'] = otp; + apiPayload['otp'] = otp; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -1237,7 +1237,7 @@ export class Account extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -1287,7 +1287,7 @@ export class Account extends Service { '{type}', encodeURIComponent(String(type)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; const uri = new URL(this.client.config.endpoint + apiPath); return this.client.call( @@ -1298,7 +1298,7 @@ export class Account extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -1347,7 +1347,7 @@ export class Account extends Service { '{type}', encodeURIComponent(String(type)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; const uri = new URL(this.client.config.endpoint + apiPath); return this.client.call( @@ -1358,7 +1358,7 @@ export class Account extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -1409,10 +1409,10 @@ export class Account extends Service { } const apiPath = '/account/mfa/challenges'; - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof factor !== 'undefined') { - payload['factor'] = factor; + apiPayload['factor'] = factor; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -1424,7 +1424,7 @@ export class Account extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -1474,10 +1474,10 @@ export class Account extends Service { } const apiPath = '/account/mfa/challenges'; - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof factor !== 'undefined') { - payload['factor'] = factor; + apiPayload['factor'] = factor; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -1489,7 +1489,7 @@ export class Account extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -1555,14 +1555,14 @@ export class Account extends Service { } const apiPath = '/account/mfa/challenges'; - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof challengeId !== 'undefined') { - payload['challengeId'] = challengeId; + apiPayload['challengeId'] = challengeId; } if (typeof otp !== 'undefined') { - payload['otp'] = otp; + apiPayload['otp'] = otp; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -1574,7 +1574,7 @@ export class Account extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -1639,14 +1639,14 @@ export class Account extends Service { } const apiPath = '/account/mfa/challenges'; - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof challengeId !== 'undefined') { - payload['challengeId'] = challengeId; + apiPayload['challengeId'] = challengeId; } if (typeof otp !== 'undefined') { - payload['otp'] = otp; + apiPayload['otp'] = otp; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -1658,7 +1658,7 @@ export class Account extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -1671,7 +1671,7 @@ export class Account extends Service { */ listMfaFactors(): Promise { const apiPath = '/account/mfa/factors'; - const payload: Payload = {}; + const apiPayload: Payload = {}; const uri = new URL(this.client.config.endpoint + apiPath); return this.client.call( @@ -1681,7 +1681,7 @@ export class Account extends Service { 'X-Appwrite-Project': this.client.config.project, accept: 'application/json', }, - payload, + apiPayload, ); } @@ -1693,7 +1693,7 @@ export class Account extends Service { */ listMFAFactors(): Promise { const apiPath = '/account/mfa/factors'; - const payload: Payload = {}; + const apiPayload: Payload = {}; const uri = new URL(this.client.config.endpoint + apiPath); return this.client.call( @@ -1703,7 +1703,7 @@ export class Account extends Service { 'X-Appwrite-Project': this.client.config.project, accept: 'application/json', }, - payload, + apiPayload, ); } @@ -1716,7 +1716,7 @@ export class Account extends Service { */ getMfaRecoveryCodes(): Promise { const apiPath = '/account/mfa/recovery-codes'; - const payload: Payload = {}; + const apiPayload: Payload = {}; const uri = new URL(this.client.config.endpoint + apiPath); return this.client.call( @@ -1726,7 +1726,7 @@ export class Account extends Service { 'X-Appwrite-Project': this.client.config.project, accept: 'application/json', }, - payload, + apiPayload, ); } @@ -1738,7 +1738,7 @@ export class Account extends Service { */ getMFARecoveryCodes(): Promise { const apiPath = '/account/mfa/recovery-codes'; - const payload: Payload = {}; + const apiPayload: Payload = {}; const uri = new URL(this.client.config.endpoint + apiPath); return this.client.call( @@ -1748,7 +1748,7 @@ export class Account extends Service { 'X-Appwrite-Project': this.client.config.project, accept: 'application/json', }, - payload, + apiPayload, ); } @@ -1761,7 +1761,7 @@ export class Account extends Service { */ createMfaRecoveryCodes(): Promise { const apiPath = '/account/mfa/recovery-codes'; - const payload: Payload = {}; + const apiPayload: Payload = {}; const uri = new URL(this.client.config.endpoint + apiPath); return this.client.call( @@ -1772,7 +1772,7 @@ export class Account extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -1784,7 +1784,7 @@ export class Account extends Service { */ createMFARecoveryCodes(): Promise { const apiPath = '/account/mfa/recovery-codes'; - const payload: Payload = {}; + const apiPayload: Payload = {}; const uri = new URL(this.client.config.endpoint + apiPath); return this.client.call( @@ -1795,7 +1795,7 @@ export class Account extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -1808,7 +1808,7 @@ export class Account extends Service { */ updateMfaRecoveryCodes(): Promise { const apiPath = '/account/mfa/recovery-codes'; - const payload: Payload = {}; + const apiPayload: Payload = {}; const uri = new URL(this.client.config.endpoint + apiPath); return this.client.call( @@ -1819,7 +1819,7 @@ export class Account extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -1831,7 +1831,7 @@ export class Account extends Service { */ updateMFARecoveryCodes(): Promise { const apiPath = '/account/mfa/recovery-codes'; - const payload: Payload = {}; + const apiPayload: Payload = {}; const uri = new URL(this.client.config.endpoint + apiPath); return this.client.call( @@ -1842,7 +1842,7 @@ export class Account extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -1893,10 +1893,10 @@ export class Account extends Service { } const apiPath = '/account/name'; - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof name !== 'undefined') { - payload['name'] = name; + apiPayload['name'] = name; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -1908,7 +1908,7 @@ export class Account extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -1975,14 +1975,14 @@ export class Account extends Service { } const apiPath = '/account/password'; - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof password !== 'undefined') { - payload['password'] = password; + apiPayload['password'] = password; } if (typeof oldPassword !== 'undefined') { - payload['oldPassword'] = oldPassword; + apiPayload['oldPassword'] = oldPassword; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -1994,7 +1994,7 @@ export class Account extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -2062,14 +2062,14 @@ export class Account extends Service { } const apiPath = '/account/phone'; - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof phone !== 'undefined') { - payload['phone'] = phone; + apiPayload['phone'] = phone; } if (typeof password !== 'undefined') { - payload['password'] = password; + apiPayload['password'] = password; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -2081,7 +2081,7 @@ export class Account extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -2095,7 +2095,7 @@ export class Account extends Service { Preferences extends Models.Preferences = Models.DefaultPreferences, >(): Promise { const apiPath = '/account/prefs'; - const payload: Payload = {}; + const apiPayload: Payload = {}; const uri = new URL(this.client.config.endpoint + apiPath); return this.client.call( @@ -2105,7 +2105,7 @@ export class Account extends Service { 'X-Appwrite-Project': this.client.config.project, accept: 'application/json', }, - payload, + apiPayload, ); } @@ -2159,10 +2159,10 @@ export class Account extends Service { } const apiPath = '/account/prefs'; - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof prefs !== 'undefined') { - payload['prefs'] = prefs; + apiPayload['prefs'] = prefs; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -2174,7 +2174,7 @@ export class Account extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -2231,14 +2231,14 @@ export class Account extends Service { } const apiPath = '/account/recovery'; - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof email !== 'undefined') { - payload['email'] = email; + apiPayload['email'] = email; } if (typeof url !== 'undefined') { - payload['url'] = url; + apiPayload['url'] = url; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -2250,7 +2250,7 @@ export class Account extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -2331,18 +2331,18 @@ export class Account extends Service { } const apiPath = '/account/recovery'; - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof userId !== 'undefined') { - payload['userId'] = userId; + apiPayload['userId'] = userId; } if (typeof secret !== 'undefined') { - payload['secret'] = secret; + apiPayload['secret'] = secret; } if (typeof password !== 'undefined') { - payload['password'] = password; + apiPayload['password'] = password; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -2354,7 +2354,7 @@ export class Account extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -2416,14 +2416,14 @@ export class Account extends Service { } const apiPath = '/account/recovery/otp'; - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof email !== 'undefined') { - payload['email'] = email; + apiPayload['email'] = email; } if (typeof phrase !== 'undefined') { - payload['phrase'] = phrase; + apiPayload['phrase'] = phrase; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -2435,7 +2435,7 @@ export class Account extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -2514,18 +2514,18 @@ export class Account extends Service { } const apiPath = '/account/recovery/otp'; - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof userId !== 'undefined') { - payload['userId'] = userId; + apiPayload['userId'] = userId; } if (typeof secret !== 'undefined') { - payload['secret'] = secret; + apiPayload['secret'] = secret; } if (typeof password !== 'undefined') { - payload['password'] = password; + apiPayload['password'] = password; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -2537,19 +2537,53 @@ export class Account extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } /** * Get the list of active sessions across different devices for the currently logged in user. * + * @param {boolean} params.total - When set to false, the total count returned will be 0 and will not be calculated. * @throws {AppwriteException} * @returns {Promise} */ - listSessions(): Promise { + listSessions(params?: { total?: boolean }): Promise; + /** + * Get the list of active sessions across different devices for the currently logged in user. + * + * @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. + */ + listSessions(total?: boolean): Promise; + listSessions( + paramsOrFirst?: { total?: boolean } | boolean, + ): Promise { + let params: { total?: boolean }; + + if ( + typeof paramsOrFirst === 'undefined' || + (paramsOrFirst && + typeof paramsOrFirst === 'object' && + !Array.isArray(paramsOrFirst)) + ) { + params = (paramsOrFirst || {}) as { total?: boolean }; + } else { + params = { + total: paramsOrFirst as boolean, + }; + } + + const total = params.total; + const apiPath = '/account/sessions'; - const payload: Payload = {}; + const apiPayload: Payload = {}; + + if (typeof total !== 'undefined') { + apiPayload['total'] = total; + } const uri = new URL(this.client.config.endpoint + apiPath); return this.client.call( @@ -2559,19 +2593,53 @@ export class Account extends Service { 'X-Appwrite-Project': this.client.config.project, accept: 'application/json', }, - payload, + apiPayload, ); } /** - * Delete all sessions from the user account and remove any sessions cookies from the end client. + * Delete all sessions from the user account and remove any sessions cookies from the end client. Pass `current` as false to keep the session making the request and sign out of every other session. * + * @param {boolean} params.current - Delete the current session too. Use false to sign out of every other session while staying signed in on this one. * @throws {AppwriteException} * @returns {Promise} */ - deleteSessions(): Promise<{}> { + deleteSessions(params?: { current?: boolean }): Promise<{}>; + /** + * Delete all sessions from the user account and remove any sessions cookies from the end client. Pass `current` as false to keep the session making the request and sign out of every other session. + * + * @param {boolean} current - Delete the current session too. Use false to sign out of every other session while staying signed in on this one. + * @throws {AppwriteException} + * @returns {Promise<{}>} + * @deprecated Use the object parameter style method for a better developer experience. + */ + deleteSessions(current?: boolean): Promise<{}>; + deleteSessions( + paramsOrFirst?: { current?: boolean } | boolean, + ): Promise<{}> { + let params: { current?: boolean }; + + if ( + typeof paramsOrFirst === 'undefined' || + (paramsOrFirst && + typeof paramsOrFirst === 'object' && + !Array.isArray(paramsOrFirst)) + ) { + params = (paramsOrFirst || {}) as { current?: boolean }; + } else { + params = { + current: paramsOrFirst as boolean, + }; + } + + const current = params.current; + const apiPath = '/account/sessions'; - const payload: Payload = {}; + const apiPayload: Payload = {}; + + if (typeof current !== 'undefined') { + apiPayload['current'] = current; + } const uri = new URL(this.client.config.endpoint + apiPath); return this.client.call( @@ -2582,7 +2650,7 @@ export class Account extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -2594,7 +2662,7 @@ export class Account extends Service { */ createAnonymousSession(): Promise { const apiPath = '/account/sessions/anonymous'; - const payload: Payload = {}; + const apiPayload: Payload = {}; const uri = new URL(this.client.config.endpoint + apiPath); return this.client.call( @@ -2605,31 +2673,34 @@ export class Account extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } /** - * Allow the user to login into their account by providing a valid email and password combination. This route will create a new session for the user. + * Allow the user to login into their account by providing a valid email and password combination. This route will create a new session for the user. Use the optional `duration` parameter to create a shorter session, for example when the user doesn't choose "remember me". It must be at least 60 seconds and cannot exceed the project maximum session length. * * 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 {string} params.email - User email. * @param {string} params.password - User password. Must be at least 8 chars. + * @param {number} params.duration - Session length in seconds. Minimum is 60 seconds, and it cannot exceed the project maximum session length. Defaults to the project maximum session length. * @throws {AppwriteException} * @returns {Promise} */ createEmailPasswordSession(params: { email: string; password: string; + duration?: number; }): Promise; /** - * Allow the user to login into their account by providing a valid email and password combination. This route will create a new session for the user. + * Allow the user to login into their account by providing a valid email and password combination. This route will create a new session for the user. Use the optional `duration` parameter to create a shorter session, for example when the user doesn't choose "remember me". It must be at least 60 seconds and cannot exceed the project maximum session length. * * 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 {string} email - User email. * @param {string} password - User password. Must be at least 8 chars. + * @param {number} duration - Session length in seconds. Minimum is 60 seconds, and it cannot exceed the project maximum session length. Defaults to the project maximum session length. * @throws {AppwriteException} * @returns {Promise} * @deprecated Use the object parameter style method for a better developer experience. @@ -2637,12 +2708,14 @@ export class Account extends Service { createEmailPasswordSession( email: string, password: string, + duration?: number, ): Promise; createEmailPasswordSession( - paramsOrFirst: { email: string; password: string } | string, - ...rest: [string?] + paramsOrFirst: + { email: string; password: string; duration?: number } | string, + ...rest: [string?, number?] ): Promise { - let params: { email: string; password: string }; + let params: { email: string; password: string; duration?: number }; if ( paramsOrFirst && @@ -2652,16 +2725,19 @@ export class Account extends Service { params = (paramsOrFirst || {}) as { email: string; password: string; + duration?: number; }; } else { params = { email: paramsOrFirst as string, password: rest[0] as string, + duration: rest[1] as number, }; } const email = params.email; const password = params.password; + const duration = params.duration; if (typeof email === 'undefined') { throw new AppwriteException('Missing required parameter: "email"'); @@ -2674,14 +2750,18 @@ export class Account extends Service { } const apiPath = '/account/sessions/email'; - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof email !== 'undefined') { - payload['email'] = email; + apiPayload['email'] = email; } if (typeof password !== 'undefined') { - payload['password'] = password; + apiPayload['password'] = password; + } + + if (typeof duration !== 'undefined') { + apiPayload['duration'] = duration; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -2693,7 +2773,7 @@ export class Account extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -2832,30 +2912,30 @@ export class Account extends Service { } const apiPath = '/account/sessions/id-token'; - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof provider !== 'undefined') { - payload['provider'] = provider; + apiPayload['provider'] = provider; } if (typeof idToken !== 'undefined') { - payload['idToken'] = idToken; + apiPayload['idToken'] = idToken; } if (typeof nonce !== 'undefined') { - payload['nonce'] = nonce; + apiPayload['nonce'] = nonce; } if (typeof accessToken !== 'undefined') { - payload['accessToken'] = accessToken; + apiPayload['accessToken'] = accessToken; } if (typeof accessTokenExpiry !== 'undefined') { - payload['accessTokenExpiry'] = accessTokenExpiry; + apiPayload['accessTokenExpiry'] = accessTokenExpiry; } if (typeof name !== 'undefined') { - payload['name'] = name; + apiPayload['name'] = name; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -2867,7 +2947,7 @@ export class Account extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -2931,14 +3011,14 @@ export class Account extends Service { } const apiPath = '/account/sessions/magic-url'; - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof userId !== 'undefined') { - payload['userId'] = userId; + apiPayload['userId'] = userId; } if (typeof secret !== 'undefined') { - payload['secret'] = secret; + apiPayload['secret'] = secret; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -2950,7 +3030,7 @@ export class Account extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -2962,7 +3042,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, appwrite, auth0, authentik, autodesk, bitbucket, bitly, box, cloudflare, dailymotion, discord, disqus, dropbox, etsy, facebook, figma, fusionauth, github, gitlab, google, huggingface, kakao, keycloak, kick, linkedin, microsoft, notion, oidc, okta, paypal, paypalSandbox, podio, resend, salesforce, slack, spotify, stripe, tiktok, tradeshift, tradeshiftBox, twitch, wordpress, x, yahoo, yammer, yandex, zoho, zoom. + * @param {OAuthProvider} params.provider - OAuth2 Provider. Currently, supported providers are: amazon, apple, appwrite, auth0, authentik, autodesk, bitbucket, bitly, box, cloudflare, dailymotion, discord, disqus, dropbox, etsy, facebook, figma, fusionauth, github, gitlab, google, huggingface, kakao, keycloak, kick, linkedin, microsoft, notion, oidc, okta, paypal, paypalSandbox, podio, resend, salesforce, slack, spotify, stripe, tiktok, tradeshift, tradeshiftBox, twitch, webflow, 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. @@ -2983,7 +3063,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, appwrite, auth0, authentik, autodesk, bitbucket, bitly, box, cloudflare, dailymotion, discord, disqus, dropbox, etsy, facebook, figma, fusionauth, github, gitlab, google, huggingface, kakao, keycloak, kick, linkedin, microsoft, notion, oidc, okta, paypal, paypalSandbox, podio, resend, salesforce, slack, spotify, stripe, tiktok, tradeshift, tradeshiftBox, twitch, wordpress, x, yahoo, yammer, yandex, zoho, zoom. + * @param {OAuthProvider} provider - OAuth2 Provider. Currently, supported providers are: amazon, apple, appwrite, auth0, authentik, autodesk, bitbucket, bitly, box, cloudflare, dailymotion, discord, disqus, dropbox, etsy, facebook, figma, fusionauth, github, gitlab, google, huggingface, kakao, keycloak, kick, linkedin, microsoft, notion, oidc, okta, paypal, paypalSandbox, podio, resend, salesforce, slack, spotify, stripe, tiktok, tradeshift, tradeshiftBox, twitch, webflow, 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. @@ -3054,24 +3134,26 @@ export class Account extends Service { '{provider}', encodeURIComponent(String(provider)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof success !== 'undefined') { - payload['success'] = success; + apiPayload['success'] = success; } if (typeof failure !== 'undefined') { - payload['failure'] = failure; + apiPayload['failure'] = failure; } if (typeof scopes !== 'undefined') { - payload['scopes'] = scopes; + apiPayload['scopes'] = scopes; } const uri = new URL(this.client.config.endpoint + apiPath); - payload['project'] = this.client.config.project; + apiPayload['project'] = this.client.config.project; - for (const [key, value] of Object.entries(Service.flatten(payload))) { + for (const [key, value] of Object.entries( + Service.flatten(apiPayload), + )) { uri.searchParams.append(key, value); } return uri; @@ -3134,14 +3216,14 @@ export class Account extends Service { } const apiPath = '/account/sessions/phone'; - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof userId !== 'undefined') { - payload['userId'] = userId; + apiPayload['userId'] = userId; } if (typeof secret !== 'undefined') { - payload['secret'] = secret; + apiPayload['secret'] = secret; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -3153,7 +3235,7 @@ export class Account extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -3213,14 +3295,14 @@ export class Account extends Service { } const apiPath = '/account/sessions/token'; - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof userId !== 'undefined') { - payload['userId'] = userId; + apiPayload['userId'] = userId; } if (typeof secret !== 'undefined') { - payload['secret'] = secret; + apiPayload['secret'] = secret; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -3232,7 +3314,7 @@ export class Account extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -3282,7 +3364,7 @@ export class Account extends Service { '{sessionId}', encodeURIComponent(String(sessionId)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; const uri = new URL(this.client.config.endpoint + apiPath); return this.client.call( @@ -3292,7 +3374,7 @@ export class Account extends Service { 'X-Appwrite-Project': this.client.config.project, accept: 'application/json', }, - payload, + apiPayload, ); } @@ -3342,7 +3424,7 @@ export class Account extends Service { '{sessionId}', encodeURIComponent(String(sessionId)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; const uri = new URL(this.client.config.endpoint + apiPath); return this.client.call( @@ -3353,7 +3435,7 @@ export class Account extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -3401,7 +3483,7 @@ export class Account extends Service { '{sessionId}', encodeURIComponent(String(sessionId)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; const uri = new URL(this.client.config.endpoint + apiPath); return this.client.call( @@ -3412,7 +3494,7 @@ export class Account extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -3426,7 +3508,7 @@ export class Account extends Service { Preferences extends Models.Preferences = Models.DefaultPreferences, >(): Promise> { const apiPath = '/account/status'; - const payload: Payload = {}; + const apiPayload: Payload = {}; const uri = new URL(this.client.config.endpoint + apiPath); return this.client.call( @@ -3437,7 +3519,7 @@ export class Account extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -3517,18 +3599,18 @@ export class Account extends Service { } const apiPath = '/account/targets/push'; - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof targetId !== 'undefined') { - payload['targetId'] = targetId; + apiPayload['targetId'] = targetId; } if (typeof identifier !== 'undefined') { - payload['identifier'] = identifier; + apiPayload['identifier'] = identifier; } if (typeof providerId !== 'undefined') { - payload['providerId'] = providerId; + apiPayload['providerId'] = providerId; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -3540,7 +3622,7 @@ export class Account extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -3610,10 +3692,10 @@ export class Account extends Service { '{targetId}', encodeURIComponent(String(targetId)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof identifier !== 'undefined') { - payload['identifier'] = identifier; + apiPayload['identifier'] = identifier; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -3625,7 +3707,7 @@ export class Account extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -3675,7 +3757,7 @@ export class Account extends Service { '{targetId}', encodeURIComponent(String(targetId)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; const uri = new URL(this.client.config.endpoint + apiPath); return this.client.call( @@ -3686,7 +3768,7 @@ export class Account extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -3763,18 +3845,18 @@ export class Account extends Service { } const apiPath = '/account/tokens/email'; - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof userId !== 'undefined') { - payload['userId'] = userId; + apiPayload['userId'] = userId; } if (typeof email !== 'undefined') { - payload['email'] = email; + apiPayload['email'] = email; } if (typeof phrase !== 'undefined') { - payload['phrase'] = phrase; + apiPayload['phrase'] = phrase; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -3786,7 +3868,7 @@ export class Account extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -3876,22 +3958,22 @@ export class Account extends Service { } const apiPath = '/account/tokens/magic-url'; - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof userId !== 'undefined') { - payload['userId'] = userId; + apiPayload['userId'] = userId; } if (typeof email !== 'undefined') { - payload['email'] = email; + apiPayload['email'] = email; } if (typeof url !== 'undefined') { - payload['url'] = url; + apiPayload['url'] = url; } if (typeof phrase !== 'undefined') { - payload['phrase'] = phrase; + apiPayload['phrase'] = phrase; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -3903,7 +3985,7 @@ export class Account extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -3916,7 +3998,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, appwrite, auth0, authentik, autodesk, bitbucket, bitly, box, cloudflare, dailymotion, discord, disqus, dropbox, etsy, facebook, figma, fusionauth, github, gitlab, google, huggingface, kakao, keycloak, kick, linkedin, microsoft, notion, oidc, okta, paypal, paypalSandbox, podio, resend, salesforce, slack, spotify, stripe, tiktok, tradeshift, tradeshiftBox, twitch, wordpress, x, yahoo, yammer, yandex, zoho, zoom. + * @param {OAuthProvider} params.provider - OAuth2 Provider. Currently, supported providers are: amazon, apple, appwrite, auth0, authentik, autodesk, bitbucket, bitly, box, cloudflare, dailymotion, discord, disqus, dropbox, etsy, facebook, figma, fusionauth, github, gitlab, google, huggingface, kakao, keycloak, kick, linkedin, microsoft, notion, oidc, okta, paypal, paypalSandbox, podio, resend, salesforce, slack, spotify, stripe, tiktok, tradeshift, tradeshiftBox, twitch, webflow, 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. @@ -3938,7 +4020,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, appwrite, auth0, authentik, autodesk, bitbucket, bitly, box, cloudflare, dailymotion, discord, disqus, dropbox, etsy, facebook, figma, fusionauth, github, gitlab, google, huggingface, kakao, keycloak, kick, linkedin, microsoft, notion, oidc, okta, paypal, paypalSandbox, podio, resend, salesforce, slack, spotify, stripe, tiktok, tradeshift, tradeshiftBox, twitch, wordpress, x, yahoo, yammer, yandex, zoho, zoom. + * @param {OAuthProvider} provider - OAuth2 Provider. Currently, supported providers are: amazon, apple, appwrite, auth0, authentik, autodesk, bitbucket, bitly, box, cloudflare, dailymotion, discord, disqus, dropbox, etsy, facebook, figma, fusionauth, github, gitlab, google, huggingface, kakao, keycloak, kick, linkedin, microsoft, notion, oidc, okta, paypal, paypalSandbox, podio, resend, salesforce, slack, spotify, stripe, tiktok, tradeshift, tradeshiftBox, twitch, webflow, 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. @@ -4009,24 +4091,26 @@ export class Account extends Service { '{provider}', encodeURIComponent(String(provider)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof success !== 'undefined') { - payload['success'] = success; + apiPayload['success'] = success; } if (typeof failure !== 'undefined') { - payload['failure'] = failure; + apiPayload['failure'] = failure; } if (typeof scopes !== 'undefined') { - payload['scopes'] = scopes; + apiPayload['scopes'] = scopes; } const uri = new URL(this.client.config.endpoint + apiPath); - payload['project'] = this.client.config.project; + apiPayload['project'] = this.client.config.project; - for (const [key, value] of Object.entries(Service.flatten(payload))) { + for (const [key, value] of Object.entries( + Service.flatten(apiPayload), + )) { uri.searchParams.append(key, value); } return uri; @@ -4089,14 +4173,14 @@ export class Account extends Service { } const apiPath = '/account/tokens/phone'; - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof userId !== 'undefined') { - payload['userId'] = userId; + apiPayload['userId'] = userId; } if (typeof phone !== 'undefined') { - payload['phone'] = phone; + apiPayload['phone'] = phone; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -4108,7 +4192,7 @@ export class Account extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -4159,10 +4243,10 @@ export class Account extends Service { } const apiPath = '/account/verifications/email'; - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof url !== 'undefined') { - payload['url'] = url; + apiPayload['url'] = url; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -4174,7 +4258,7 @@ export class Account extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -4226,10 +4310,10 @@ export class Account extends Service { } const apiPath = '/account/verifications/email'; - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof url !== 'undefined') { - payload['url'] = url; + apiPayload['url'] = url; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -4241,7 +4325,7 @@ export class Account extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -4304,14 +4388,14 @@ export class Account extends Service { } const apiPath = '/account/verifications/email'; - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof userId !== 'undefined') { - payload['userId'] = userId; + apiPayload['userId'] = userId; } if (typeof secret !== 'undefined') { - payload['secret'] = secret; + apiPayload['secret'] = secret; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -4323,7 +4407,7 @@ export class Account extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -4384,14 +4468,14 @@ export class Account extends Service { } const apiPath = '/account/verifications/email'; - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof userId !== 'undefined') { - payload['userId'] = userId; + apiPayload['userId'] = userId; } if (typeof secret !== 'undefined') { - payload['secret'] = secret; + apiPayload['secret'] = secret; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -4403,7 +4487,7 @@ export class Account extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -4453,10 +4537,10 @@ export class Account extends Service { const phrase = params.phrase; const apiPath = '/account/verifications/email/otp'; - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof phrase !== 'undefined') { - payload['phrase'] = phrase; + apiPayload['phrase'] = phrase; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -4468,7 +4552,7 @@ export class Account extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -4533,14 +4617,14 @@ export class Account extends Service { } const apiPath = '/account/verifications/email/otp'; - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof userId !== 'undefined') { - payload['userId'] = userId; + apiPayload['userId'] = userId; } if (typeof secret !== 'undefined') { - payload['secret'] = secret; + apiPayload['secret'] = secret; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -4552,7 +4636,7 @@ export class Account extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -4564,7 +4648,7 @@ export class Account extends Service { */ createPhoneVerification(): Promise { const apiPath = '/account/verifications/phone'; - const payload: Payload = {}; + const apiPayload: Payload = {}; const uri = new URL(this.client.config.endpoint + apiPath); return this.client.call( @@ -4575,7 +4659,7 @@ export class Account extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -4638,14 +4722,14 @@ export class Account extends Service { } const apiPath = '/account/verifications/phone'; - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof userId !== 'undefined') { - payload['userId'] = userId; + apiPayload['userId'] = userId; } if (typeof secret !== 'undefined') { - payload['secret'] = secret; + apiPayload['secret'] = secret; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -4657,7 +4741,7 @@ export class Account extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } } diff --git a/src/services/apps.ts b/src/services/apps.ts new file mode 100644 index 00000000..02554ba4 --- /dev/null +++ b/src/services/apps.ts @@ -0,0 +1,1946 @@ +import { Service } from '../service'; +import { AppwriteException, Client } from '../client'; +import type { Models } from '../models'; +import type { Payload } from '../client'; + +export class Apps extends Service { + constructor(client: Client) { + super(client); + } + + /** + * List applications. + * + * @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. + * @param {boolean} params.total - When set to false, the total count returned will be 0 and will not be calculated. + * @throws {AppwriteException} + * @returns {Promise} + */ + list(params?: { + queries?: string[]; + total?: boolean; + }): Promise; + /** + * List applications. + * + * @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. + * @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. + */ + list(queries?: string[], total?: boolean): Promise; + list( + paramsOrFirst?: { queries?: string[]; total?: boolean } | string[], + ...rest: [boolean?] + ): Promise { + let params: { queries?: string[]; total?: boolean }; + + if ( + (typeof paramsOrFirst === 'undefined' && rest.length === 0) || + (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 = '/apps'; + const apiPayload: Payload = {}; + + if (typeof queries !== 'undefined') { + apiPayload['queries'] = queries; + } + + if (typeof total !== 'undefined') { + apiPayload['total'] = total; + } + + const uri = new URL(this.client.config.endpoint + apiPath); + return this.client.call( + 'get', + uri, + { + 'X-Appwrite-Project': this.client.config.project, + accept: 'application/json', + }, + apiPayload, + ); + } + + /** + * Create a new application. + * + * @param {string} params.appId - Application ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can't start with a special char. Max length is 36 chars. + * @param {string} params.name - Application name. + * @param {string[]} params.redirectUris - Redirect URIs. Each must be an https URL, an http loopback URL (localhost, 127.0.0.1, [::1]), or a private-use scheme URI (e.g. com.example.app:/oauth), and must not contain a fragment. + * @param {string} params.description - Application description shown to users during OAuth2 consent. + * @param {string} params.clientUri - Application homepage URL shown to users during OAuth2 consent. + * @param {string} params.logoUri - Application logo URL shown to users during OAuth2 consent. + * @param {string} params.privacyPolicyUrl - Application privacy policy URL shown to users during OAuth2 consent. + * @param {string} params.termsUrl - Application terms of service URL shown to users during OAuth2 consent. + * @param {string[]} params.contacts - Application support or security contact emails. Maximum of 100 contacts are allowed. + * @param {string} params.tagline - Application tagline shown to users during OAuth2 consent. + * @param {string[]} params.tags - Application tags shown to users during OAuth2 consent. Maximum of 100 tags are allowed, each up to 64 characters long. + * @param {string[]} params.images - Application image URLs shown to users during OAuth2 consent. Maximum of 100 images are allowed. + * @param {string} params.supportUrl - Application support URL shown to users during OAuth2 consent. + * @param {string} params.dataDeletionUrl - Application data deletion URL shown to users during OAuth2 consent. + * @param {string[]} params.postLogoutRedirectUris - Post-logout redirect URIs for OpenID Connect RP-Initiated Logout. Each must be an https URL, an http loopback URL, or a private-use scheme URI, and must not contain a fragment. After ending the user session, the logout endpoint only redirects to URIs in this list. + * @param {boolean} params.enabled - Is application enabled? + * @param {string} params.type - OAuth2 client type. Use `public` for SPAs, mobile, and native apps that cannot keep a `client_secret` — PKCE is then required at the token endpoint. Use `confidential` for server-side clients that present a `client_secret`. Defaults to `confidential`. + * @param {boolean} params.deviceFlow - Allow this client to use the OAuth2 Device Authorization Grant (RFC 8628) for input-constrained devices such as TVs and CLIs. Defaults to false. + * @param {string} params.teamId - Team unique ID. + * @throws {AppwriteException} + * @returns {Promise} + */ + create(params: { + appId: string; + name: string; + redirectUris: string[]; + description?: string; + clientUri?: string; + logoUri?: string; + privacyPolicyUrl?: string; + termsUrl?: string; + contacts?: string[]; + tagline?: string; + tags?: string[]; + images?: string[]; + supportUrl?: string; + dataDeletionUrl?: string; + postLogoutRedirectUris?: string[]; + enabled?: boolean; + type?: string; + deviceFlow?: boolean; + teamId?: string; + }): Promise; + /** + * Create a new application. + * + * @param {string} appId - Application ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can't start with a special char. Max length is 36 chars. + * @param {string} name - Application name. + * @param {string[]} redirectUris - Redirect URIs. Each must be an https URL, an http loopback URL (localhost, 127.0.0.1, [::1]), or a private-use scheme URI (e.g. com.example.app:/oauth), and must not contain a fragment. + * @param {string} description - Application description shown to users during OAuth2 consent. + * @param {string} clientUri - Application homepage URL shown to users during OAuth2 consent. + * @param {string} logoUri - Application logo URL shown to users during OAuth2 consent. + * @param {string} privacyPolicyUrl - Application privacy policy URL shown to users during OAuth2 consent. + * @param {string} termsUrl - Application terms of service URL shown to users during OAuth2 consent. + * @param {string[]} contacts - Application support or security contact emails. Maximum of 100 contacts are allowed. + * @param {string} tagline - Application tagline shown to users during OAuth2 consent. + * @param {string[]} tags - Application tags shown to users during OAuth2 consent. Maximum of 100 tags are allowed, each up to 64 characters long. + * @param {string[]} images - Application image URLs shown to users during OAuth2 consent. Maximum of 100 images are allowed. + * @param {string} supportUrl - Application support URL shown to users during OAuth2 consent. + * @param {string} dataDeletionUrl - Application data deletion URL shown to users during OAuth2 consent. + * @param {string[]} postLogoutRedirectUris - Post-logout redirect URIs for OpenID Connect RP-Initiated Logout. Each must be an https URL, an http loopback URL, or a private-use scheme URI, and must not contain a fragment. After ending the user session, the logout endpoint only redirects to URIs in this list. + * @param {boolean} enabled - Is application enabled? + * @param {string} type - OAuth2 client type. Use `public` for SPAs, mobile, and native apps that cannot keep a `client_secret` — PKCE is then required at the token endpoint. Use `confidential` for server-side clients that present a `client_secret`. Defaults to `confidential`. + * @param {boolean} deviceFlow - Allow this client to use the OAuth2 Device Authorization Grant (RFC 8628) for input-constrained devices such as TVs and CLIs. Defaults to false. + * @param {string} teamId - Team unique ID. + * @throws {AppwriteException} + * @returns {Promise} + * @deprecated Use the object parameter style method for a better developer experience. + */ + create( + appId: string, + name: string, + redirectUris: string[], + description?: string, + clientUri?: string, + logoUri?: string, + privacyPolicyUrl?: string, + termsUrl?: string, + contacts?: string[], + tagline?: string, + tags?: string[], + images?: string[], + supportUrl?: string, + dataDeletionUrl?: string, + postLogoutRedirectUris?: string[], + enabled?: boolean, + type?: string, + deviceFlow?: boolean, + teamId?: string, + ): Promise; + create( + paramsOrFirst: + | { + appId: string; + name: string; + redirectUris: string[]; + description?: string; + clientUri?: string; + logoUri?: string; + privacyPolicyUrl?: string; + termsUrl?: string; + contacts?: string[]; + tagline?: string; + tags?: string[]; + images?: string[]; + supportUrl?: string; + dataDeletionUrl?: string; + postLogoutRedirectUris?: string[]; + enabled?: boolean; + type?: string; + deviceFlow?: boolean; + teamId?: string; + } + | string, + ...rest: [ + string?, + string[]?, + string?, + string?, + string?, + string?, + string?, + string[]?, + string?, + string[]?, + string[]?, + string?, + string?, + string[]?, + boolean?, + string?, + boolean?, + string?, + ] + ): Promise { + let params: { + appId: string; + name: string; + redirectUris: string[]; + description?: string; + clientUri?: string; + logoUri?: string; + privacyPolicyUrl?: string; + termsUrl?: string; + contacts?: string[]; + tagline?: string; + tags?: string[]; + images?: string[]; + supportUrl?: string; + dataDeletionUrl?: string; + postLogoutRedirectUris?: string[]; + enabled?: boolean; + type?: string; + deviceFlow?: boolean; + teamId?: string; + }; + + if ( + paramsOrFirst && + typeof paramsOrFirst === 'object' && + !Array.isArray(paramsOrFirst) + ) { + params = (paramsOrFirst || {}) as { + appId: string; + name: string; + redirectUris: string[]; + description?: string; + clientUri?: string; + logoUri?: string; + privacyPolicyUrl?: string; + termsUrl?: string; + contacts?: string[]; + tagline?: string; + tags?: string[]; + images?: string[]; + supportUrl?: string; + dataDeletionUrl?: string; + postLogoutRedirectUris?: string[]; + enabled?: boolean; + type?: string; + deviceFlow?: boolean; + teamId?: string; + }; + } else { + params = { + appId: paramsOrFirst as string, + name: rest[0] as string, + redirectUris: rest[1] as string[], + description: rest[2] as string, + clientUri: rest[3] as string, + logoUri: rest[4] as string, + privacyPolicyUrl: rest[5] as string, + termsUrl: rest[6] as string, + contacts: rest[7] as string[], + tagline: rest[8] as string, + tags: rest[9] as string[], + images: rest[10] as string[], + supportUrl: rest[11] as string, + dataDeletionUrl: rest[12] as string, + postLogoutRedirectUris: rest[13] as string[], + enabled: rest[14] as boolean, + type: rest[15] as string, + deviceFlow: rest[16] as boolean, + teamId: rest[17] as string, + }; + } + + const appId = params.appId; + const name = params.name; + const redirectUris = params.redirectUris; + const description = params.description; + const clientUri = params.clientUri; + const logoUri = params.logoUri; + const privacyPolicyUrl = params.privacyPolicyUrl; + const termsUrl = params.termsUrl; + const contacts = params.contacts; + const tagline = params.tagline; + const tags = params.tags; + const images = params.images; + const supportUrl = params.supportUrl; + const dataDeletionUrl = params.dataDeletionUrl; + const postLogoutRedirectUris = params.postLogoutRedirectUris; + const enabled = params.enabled; + const type = params.type; + const deviceFlow = params.deviceFlow; + const teamId = params.teamId; + + if (typeof appId === 'undefined') { + throw new AppwriteException('Missing required parameter: "appId"'); + } + + if (typeof name === 'undefined') { + throw new AppwriteException('Missing required parameter: "name"'); + } + + if (typeof redirectUris === 'undefined') { + throw new AppwriteException( + 'Missing required parameter: "redirectUris"', + ); + } + + const apiPath = '/apps'; + const apiPayload: Payload = {}; + + if (typeof appId !== 'undefined') { + apiPayload['appId'] = appId; + } + + if (typeof name !== 'undefined') { + apiPayload['name'] = name; + } + + if (typeof description !== 'undefined') { + apiPayload['description'] = description; + } + + if (typeof clientUri !== 'undefined') { + apiPayload['clientUri'] = clientUri; + } + + if (typeof logoUri !== 'undefined') { + apiPayload['logoUri'] = logoUri; + } + + if (typeof privacyPolicyUrl !== 'undefined') { + apiPayload['privacyPolicyUrl'] = privacyPolicyUrl; + } + + if (typeof termsUrl !== 'undefined') { + apiPayload['termsUrl'] = termsUrl; + } + + if (typeof contacts !== 'undefined') { + apiPayload['contacts'] = contacts; + } + + if (typeof tagline !== 'undefined') { + apiPayload['tagline'] = tagline; + } + + if (typeof tags !== 'undefined') { + apiPayload['tags'] = tags; + } + + if (typeof images !== 'undefined') { + apiPayload['images'] = images; + } + + if (typeof supportUrl !== 'undefined') { + apiPayload['supportUrl'] = supportUrl; + } + + if (typeof dataDeletionUrl !== 'undefined') { + apiPayload['dataDeletionUrl'] = dataDeletionUrl; + } + + if (typeof redirectUris !== 'undefined') { + apiPayload['redirectUris'] = redirectUris; + } + + if (typeof postLogoutRedirectUris !== 'undefined') { + apiPayload['postLogoutRedirectUris'] = postLogoutRedirectUris; + } + + if (typeof enabled !== 'undefined') { + apiPayload['enabled'] = enabled; + } + + if (typeof type !== 'undefined') { + apiPayload['type'] = type; + } + + if (typeof deviceFlow !== 'undefined') { + apiPayload['deviceFlow'] = deviceFlow; + } + + if (typeof teamId !== 'undefined') { + apiPayload['teamId'] = teamId; + } + + const uri = new URL(this.client.config.endpoint + apiPath); + return this.client.call( + 'post', + uri, + { + 'X-Appwrite-Project': this.client.config.project, + 'content-type': 'application/json', + accept: 'application/json', + }, + apiPayload, + ); + } + + /** + * List scopes an application can request when installed on a team. + * + * @throws {AppwriteException} + * @returns {Promise} + */ + listInstallationScopes(): Promise { + const apiPath = '/apps/scopes/installations'; + const apiPayload: Payload = {}; + + const uri = new URL(this.client.config.endpoint + apiPath); + return this.client.call( + 'get', + uri, + { + 'X-Appwrite-Project': this.client.config.project, + accept: 'application/json', + }, + apiPayload, + ); + } + + /** + * List scopes an application can request during the OAuth2 flow. + * + * @throws {AppwriteException} + * @returns {Promise} + */ + listOAuth2Scopes(): Promise { + const apiPath = '/apps/scopes/oauth2'; + const apiPayload: Payload = {}; + + const uri = new URL(this.client.config.endpoint + apiPath); + return this.client.call( + 'get', + uri, + { + 'X-Appwrite-Project': this.client.config.project, + accept: 'application/json', + }, + apiPayload, + ); + } + + /** + * Get an application by its unique ID. + * + * @param {string} params.appId - Application unique ID. + * @throws {AppwriteException} + * @returns {Promise} + */ + get(params: { appId: string }): Promise; + /** + * Get an application by its unique ID. + * + * @param {string} appId - Application unique ID. + * @throws {AppwriteException} + * @returns {Promise} + * @deprecated Use the object parameter style method for a better developer experience. + */ + get(appId: string): Promise; + get(paramsOrFirst: { appId: string } | string): Promise { + let params: { appId: string }; + + if ( + paramsOrFirst && + typeof paramsOrFirst === 'object' && + !Array.isArray(paramsOrFirst) + ) { + params = (paramsOrFirst || {}) as { appId: string }; + } else { + params = { + appId: paramsOrFirst as string, + }; + } + + const appId = params.appId; + + if (typeof appId === 'undefined' || appId === '') { + throw new AppwriteException('Missing required parameter: "appId"'); + } + + const apiPath = '/apps/{appId}'.replace( + '{appId}', + encodeURIComponent(String(appId)), + ); + const apiPayload: Payload = {}; + + const uri = new URL(this.client.config.endpoint + apiPath); + return this.client.call( + 'get', + uri, + { + 'X-Appwrite-Project': this.client.config.project, + accept: 'application/json', + }, + apiPayload, + ); + } + + /** + * Update an application by its unique ID. + * + * @param {string} params.appId - Application unique ID. + * @param {string} params.name - Application name. + * @param {string} params.description - Application description shown to users during OAuth2 consent. + * @param {string} params.clientUri - Application homepage URL shown to users during OAuth2 consent. + * @param {string} params.logoUri - Application logo URL shown to users during OAuth2 consent. + * @param {string} params.privacyPolicyUrl - Application privacy policy URL shown to users during OAuth2 consent. + * @param {string} params.termsUrl - Application terms of service URL shown to users during OAuth2 consent. + * @param {string[]} params.contacts - Application support or security contact emails. Maximum of 100 contacts are allowed. + * @param {string} params.tagline - Application tagline shown to users during OAuth2 consent. + * @param {string[]} params.tags - Application tags shown to users during OAuth2 consent. Maximum of 100 tags are allowed, each up to 64 characters long. + * @param {string[]} params.images - Application image URLs shown to users during OAuth2 consent. Maximum of 100 images are allowed. + * @param {string} params.supportUrl - Application support URL shown to users during OAuth2 consent. + * @param {string} params.dataDeletionUrl - Application data deletion URL shown to users during OAuth2 consent. + * @param {boolean} params.enabled - Is application enabled? + * @param {string[]} params.redirectUris - Redirect URIs. Each must be an https URL, an http loopback URL (localhost, 127.0.0.1, [::1]), or a private-use scheme URI (e.g. com.example.app:/oauth), and must not contain a fragment. + * @param {string[]} params.postLogoutRedirectUris - Post-logout redirect URIs for OpenID Connect RP-Initiated Logout. Each must be an https URL, an http loopback URL, or a private-use scheme URI, and must not contain a fragment. After ending the user session, the logout endpoint only redirects to URIs in this list. + * @param {string} params.type - OAuth2 client type. Use `public` for SPAs, mobile, and native apps that cannot keep a `client_secret` — PKCE is then required at the token endpoint. Use `confidential` for server-side clients that present a `client_secret`. Defaults to `confidential`. + * @param {boolean} params.deviceFlow - Allow this client to use the OAuth2 Device Authorization Grant (RFC 8628) for input-constrained devices such as TVs and CLIs. Defaults to false. + * @param {string[]} params.installationScopes - Scopes the application requests when installed on a team. Only scopes allowed by the project's OAuth2 server installation scopes configuration are accepted; use the list installation scopes endpoint to discover available values. Maximum of 100 scopes are allowed. + * @param {string} params.installationRedirectUrl - URL users are redirected to after creating or updating an installation of this application. Must be an https URL, an http loopback URL (localhost, 127.0.0.1, [::1]), or a private-use scheme URI, and must not contain a fragment. Leave empty for no redirect. + * @throws {AppwriteException} + * @returns {Promise} + */ + update(params: { + appId: string; + name: string; + description?: string; + clientUri?: string; + logoUri?: string; + privacyPolicyUrl?: string; + termsUrl?: string; + contacts?: string[]; + tagline?: string; + tags?: string[]; + images?: string[]; + supportUrl?: string; + dataDeletionUrl?: string; + enabled?: boolean; + redirectUris?: string[]; + postLogoutRedirectUris?: string[]; + type?: string; + deviceFlow?: boolean; + installationScopes?: string[]; + installationRedirectUrl?: string; + }): Promise; + /** + * Update an application by its unique ID. + * + * @param {string} appId - Application unique ID. + * @param {string} name - Application name. + * @param {string} description - Application description shown to users during OAuth2 consent. + * @param {string} clientUri - Application homepage URL shown to users during OAuth2 consent. + * @param {string} logoUri - Application logo URL shown to users during OAuth2 consent. + * @param {string} privacyPolicyUrl - Application privacy policy URL shown to users during OAuth2 consent. + * @param {string} termsUrl - Application terms of service URL shown to users during OAuth2 consent. + * @param {string[]} contacts - Application support or security contact emails. Maximum of 100 contacts are allowed. + * @param {string} tagline - Application tagline shown to users during OAuth2 consent. + * @param {string[]} tags - Application tags shown to users during OAuth2 consent. Maximum of 100 tags are allowed, each up to 64 characters long. + * @param {string[]} images - Application image URLs shown to users during OAuth2 consent. Maximum of 100 images are allowed. + * @param {string} supportUrl - Application support URL shown to users during OAuth2 consent. + * @param {string} dataDeletionUrl - Application data deletion URL shown to users during OAuth2 consent. + * @param {boolean} enabled - Is application enabled? + * @param {string[]} redirectUris - Redirect URIs. Each must be an https URL, an http loopback URL (localhost, 127.0.0.1, [::1]), or a private-use scheme URI (e.g. com.example.app:/oauth), and must not contain a fragment. + * @param {string[]} postLogoutRedirectUris - Post-logout redirect URIs for OpenID Connect RP-Initiated Logout. Each must be an https URL, an http loopback URL, or a private-use scheme URI, and must not contain a fragment. After ending the user session, the logout endpoint only redirects to URIs in this list. + * @param {string} type - OAuth2 client type. Use `public` for SPAs, mobile, and native apps that cannot keep a `client_secret` — PKCE is then required at the token endpoint. Use `confidential` for server-side clients that present a `client_secret`. Defaults to `confidential`. + * @param {boolean} deviceFlow - Allow this client to use the OAuth2 Device Authorization Grant (RFC 8628) for input-constrained devices such as TVs and CLIs. Defaults to false. + * @param {string[]} installationScopes - Scopes the application requests when installed on a team. Only scopes allowed by the project's OAuth2 server installation scopes configuration are accepted; use the list installation scopes endpoint to discover available values. Maximum of 100 scopes are allowed. + * @param {string} installationRedirectUrl - URL users are redirected to after creating or updating an installation of this application. Must be an https URL, an http loopback URL (localhost, 127.0.0.1, [::1]), or a private-use scheme URI, and must not contain a fragment. Leave empty for no redirect. + * @throws {AppwriteException} + * @returns {Promise} + * @deprecated Use the object parameter style method for a better developer experience. + */ + update( + appId: string, + name: string, + description?: string, + clientUri?: string, + logoUri?: string, + privacyPolicyUrl?: string, + termsUrl?: string, + contacts?: string[], + tagline?: string, + tags?: string[], + images?: string[], + supportUrl?: string, + dataDeletionUrl?: string, + enabled?: boolean, + redirectUris?: string[], + postLogoutRedirectUris?: string[], + type?: string, + deviceFlow?: boolean, + installationScopes?: string[], + installationRedirectUrl?: string, + ): Promise; + update( + paramsOrFirst: + | { + appId: string; + name: string; + description?: string; + clientUri?: string; + logoUri?: string; + privacyPolicyUrl?: string; + termsUrl?: string; + contacts?: string[]; + tagline?: string; + tags?: string[]; + images?: string[]; + supportUrl?: string; + dataDeletionUrl?: string; + enabled?: boolean; + redirectUris?: string[]; + postLogoutRedirectUris?: string[]; + type?: string; + deviceFlow?: boolean; + installationScopes?: string[]; + installationRedirectUrl?: string; + } + | string, + ...rest: [ + string?, + string?, + string?, + string?, + string?, + string?, + string[]?, + string?, + string[]?, + string[]?, + string?, + string?, + boolean?, + string[]?, + string[]?, + string?, + boolean?, + string[]?, + string?, + ] + ): Promise { + let params: { + appId: string; + name: string; + description?: string; + clientUri?: string; + logoUri?: string; + privacyPolicyUrl?: string; + termsUrl?: string; + contacts?: string[]; + tagline?: string; + tags?: string[]; + images?: string[]; + supportUrl?: string; + dataDeletionUrl?: string; + enabled?: boolean; + redirectUris?: string[]; + postLogoutRedirectUris?: string[]; + type?: string; + deviceFlow?: boolean; + installationScopes?: string[]; + installationRedirectUrl?: string; + }; + + if ( + paramsOrFirst && + typeof paramsOrFirst === 'object' && + !Array.isArray(paramsOrFirst) + ) { + params = (paramsOrFirst || {}) as { + appId: string; + name: string; + description?: string; + clientUri?: string; + logoUri?: string; + privacyPolicyUrl?: string; + termsUrl?: string; + contacts?: string[]; + tagline?: string; + tags?: string[]; + images?: string[]; + supportUrl?: string; + dataDeletionUrl?: string; + enabled?: boolean; + redirectUris?: string[]; + postLogoutRedirectUris?: string[]; + type?: string; + deviceFlow?: boolean; + installationScopes?: string[]; + installationRedirectUrl?: string; + }; + } else { + params = { + appId: paramsOrFirst as string, + name: rest[0] as string, + description: rest[1] as string, + clientUri: rest[2] as string, + logoUri: rest[3] as string, + privacyPolicyUrl: rest[4] as string, + termsUrl: rest[5] as string, + contacts: rest[6] as string[], + tagline: rest[7] as string, + tags: rest[8] as string[], + images: rest[9] as string[], + supportUrl: rest[10] as string, + dataDeletionUrl: rest[11] as string, + enabled: rest[12] as boolean, + redirectUris: rest[13] as string[], + postLogoutRedirectUris: rest[14] as string[], + type: rest[15] as string, + deviceFlow: rest[16] as boolean, + installationScopes: rest[17] as string[], + installationRedirectUrl: rest[18] as string, + }; + } + + const appId = params.appId; + const name = params.name; + const description = params.description; + const clientUri = params.clientUri; + const logoUri = params.logoUri; + const privacyPolicyUrl = params.privacyPolicyUrl; + const termsUrl = params.termsUrl; + const contacts = params.contacts; + const tagline = params.tagline; + const tags = params.tags; + const images = params.images; + const supportUrl = params.supportUrl; + const dataDeletionUrl = params.dataDeletionUrl; + const enabled = params.enabled; + const redirectUris = params.redirectUris; + const postLogoutRedirectUris = params.postLogoutRedirectUris; + const type = params.type; + const deviceFlow = params.deviceFlow; + const installationScopes = params.installationScopes; + const installationRedirectUrl = params.installationRedirectUrl; + + if (typeof appId === 'undefined' || appId === '') { + throw new AppwriteException('Missing required parameter: "appId"'); + } + + if (typeof name === 'undefined') { + throw new AppwriteException('Missing required parameter: "name"'); + } + + const apiPath = '/apps/{appId}'.replace( + '{appId}', + encodeURIComponent(String(appId)), + ); + const apiPayload: Payload = {}; + + if (typeof name !== 'undefined') { + apiPayload['name'] = name; + } + + if (typeof description !== 'undefined') { + apiPayload['description'] = description; + } + + if (typeof clientUri !== 'undefined') { + apiPayload['clientUri'] = clientUri; + } + + if (typeof logoUri !== 'undefined') { + apiPayload['logoUri'] = logoUri; + } + + if (typeof privacyPolicyUrl !== 'undefined') { + apiPayload['privacyPolicyUrl'] = privacyPolicyUrl; + } + + if (typeof termsUrl !== 'undefined') { + apiPayload['termsUrl'] = termsUrl; + } + + if (typeof contacts !== 'undefined') { + apiPayload['contacts'] = contacts; + } + + if (typeof tagline !== 'undefined') { + apiPayload['tagline'] = tagline; + } + + if (typeof tags !== 'undefined') { + apiPayload['tags'] = tags; + } + + if (typeof images !== 'undefined') { + apiPayload['images'] = images; + } + + if (typeof supportUrl !== 'undefined') { + apiPayload['supportUrl'] = supportUrl; + } + + if (typeof dataDeletionUrl !== 'undefined') { + apiPayload['dataDeletionUrl'] = dataDeletionUrl; + } + + if (typeof enabled !== 'undefined') { + apiPayload['enabled'] = enabled; + } + + if (typeof redirectUris !== 'undefined') { + apiPayload['redirectUris'] = redirectUris; + } + + if (typeof postLogoutRedirectUris !== 'undefined') { + apiPayload['postLogoutRedirectUris'] = postLogoutRedirectUris; + } + + if (typeof type !== 'undefined') { + apiPayload['type'] = type; + } + + if (typeof deviceFlow !== 'undefined') { + apiPayload['deviceFlow'] = deviceFlow; + } + + if (typeof installationScopes !== 'undefined') { + apiPayload['installationScopes'] = installationScopes; + } + + if (typeof installationRedirectUrl !== 'undefined') { + apiPayload['installationRedirectUrl'] = installationRedirectUrl; + } + + const uri = new URL(this.client.config.endpoint + apiPath); + return this.client.call( + 'put', + uri, + { + 'X-Appwrite-Project': this.client.config.project, + 'content-type': 'application/json', + accept: 'application/json', + }, + apiPayload, + ); + } + + /** + * Delete an application by its unique ID. + * + * @param {string} params.appId - Application unique ID. + * @throws {AppwriteException} + * @returns {Promise} + */ + delete(params: { appId: string }): Promise<{}>; + /** + * Delete an application by its unique ID. + * + * @param {string} appId - Application unique ID. + * @throws {AppwriteException} + * @returns {Promise<{}>} + * @deprecated Use the object parameter style method for a better developer experience. + */ + delete(appId: string): Promise<{}>; + delete(paramsOrFirst: { appId: string } | string): Promise<{}> { + let params: { appId: string }; + + if ( + paramsOrFirst && + typeof paramsOrFirst === 'object' && + !Array.isArray(paramsOrFirst) + ) { + params = (paramsOrFirst || {}) as { appId: string }; + } else { + params = { + appId: paramsOrFirst as string, + }; + } + + const appId = params.appId; + + if (typeof appId === 'undefined' || appId === '') { + throw new AppwriteException('Missing required parameter: "appId"'); + } + + const apiPath = '/apps/{appId}'.replace( + '{appId}', + encodeURIComponent(String(appId)), + ); + const apiPayload: Payload = {}; + + const uri = new URL(this.client.config.endpoint + apiPath); + return this.client.call( + 'delete', + uri, + { + 'X-Appwrite-Project': this.client.config.project, + 'content-type': 'application/json', + accept: 'application/json', + }, + apiPayload, + ); + } + + /** + * List installations of an application. Requires an app key sent in the `X-Appwrite-Key` header alongside the `X-Appwrite-App` header, or a caller with update access to the app. + * + * @param {string} params.appId - Application unique 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. + * @param {boolean} params.total - When set to false, the total count returned will be 0 and will not be calculated. + * @throws {AppwriteException} + * @returns {Promise} + */ + listInstallations(params: { + appId: string; + queries?: string[]; + total?: boolean; + }): Promise; + /** + * List installations of an application. Requires an app key sent in the `X-Appwrite-Key` header alongside the `X-Appwrite-App` header, or a caller with update access to the app. + * + * @param {string} appId - Application unique 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. + * @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. + */ + listInstallations( + appId: string, + queries?: string[], + total?: boolean, + ): Promise; + listInstallations( + paramsOrFirst: + { appId: string; queries?: string[]; total?: boolean } | string, + ...rest: [string[]?, boolean?] + ): Promise { + let params: { appId: string; queries?: string[]; total?: boolean }; + + if ( + paramsOrFirst && + typeof paramsOrFirst === 'object' && + !Array.isArray(paramsOrFirst) + ) { + params = (paramsOrFirst || {}) as { + appId: string; + queries?: string[]; + total?: boolean; + }; + } else { + params = { + appId: paramsOrFirst as string, + queries: rest[0] as string[], + total: rest[1] as boolean, + }; + } + + const appId = params.appId; + const queries = params.queries; + const total = params.total; + + if (typeof appId === 'undefined' || appId === '') { + throw new AppwriteException('Missing required parameter: "appId"'); + } + + const apiPath = '/apps/{appId}/installations'.replace( + '{appId}', + encodeURIComponent(String(appId)), + ); + const apiPayload: Payload = {}; + + if (typeof queries !== 'undefined') { + apiPayload['queries'] = queries; + } + + if (typeof total !== 'undefined') { + apiPayload['total'] = total; + } + + const uri = new URL(this.client.config.endpoint + apiPath); + return this.client.call( + 'get', + uri, + { + 'X-Appwrite-Project': this.client.config.project, + accept: 'application/json', + }, + apiPayload, + ); + } + + /** + * Get an installation of an application by its unique ID. Requires an app key sent in the `X-Appwrite-Key` header alongside the `X-Appwrite-App` header, or a caller with update access to the app. + * + * @param {string} params.appId - Application unique ID. + * @param {string} params.installationId - Installation unique ID. + * @throws {AppwriteException} + * @returns {Promise} + */ + getInstallation(params: { + appId: string; + installationId: string; + }): Promise; + /** + * Get an installation of an application by its unique ID. Requires an app key sent in the `X-Appwrite-Key` header alongside the `X-Appwrite-App` header, or a caller with update access to the app. + * + * @param {string} appId - Application unique ID. + * @param {string} installationId - Installation unique ID. + * @throws {AppwriteException} + * @returns {Promise} + * @deprecated Use the object parameter style method for a better developer experience. + */ + getInstallation( + appId: string, + installationId: string, + ): Promise; + getInstallation( + paramsOrFirst: { appId: string; installationId: string } | string, + ...rest: [string?] + ): Promise { + let params: { appId: string; installationId: string }; + + if ( + paramsOrFirst && + typeof paramsOrFirst === 'object' && + !Array.isArray(paramsOrFirst) + ) { + params = (paramsOrFirst || {}) as { + appId: string; + installationId: string; + }; + } else { + params = { + appId: paramsOrFirst as string, + installationId: rest[0] as string, + }; + } + + const appId = params.appId; + const installationId = params.installationId; + + if (typeof appId === 'undefined' || appId === '') { + throw new AppwriteException('Missing required parameter: "appId"'); + } + + if (typeof installationId === 'undefined' || installationId === '') { + throw new AppwriteException( + 'Missing required parameter: "installationId"', + ); + } + + const apiPath = '/apps/{appId}/installations/{installationId}' + .replace('{appId}', encodeURIComponent(String(appId))) + .replace( + '{installationId}', + encodeURIComponent(String(installationId)), + ); + const apiPayload: Payload = {}; + + const uri = new URL(this.client.config.endpoint + apiPath); + return this.client.call( + 'get', + uri, + { + 'X-Appwrite-Project': this.client.config.project, + accept: 'application/json', + }, + apiPayload, + ); + } + + /** + * Delete an installation of an application by its unique ID. Requires a caller with update access to the app. Previously issued installation access tokens are revoked. + * + * @param {string} params.appId - Application unique ID. + * @param {string} params.installationId - Installation unique ID. + * @throws {AppwriteException} + * @returns {Promise} + */ + deleteInstallation(params: { + appId: string; + installationId: string; + }): Promise<{}>; + /** + * Delete an installation of an application by its unique ID. Requires a caller with update access to the app. Previously issued installation access tokens are revoked. + * + * @param {string} appId - Application unique ID. + * @param {string} installationId - Installation unique ID. + * @throws {AppwriteException} + * @returns {Promise<{}>} + * @deprecated Use the object parameter style method for a better developer experience. + */ + deleteInstallation(appId: string, installationId: string): Promise<{}>; + deleteInstallation( + paramsOrFirst: { appId: string; installationId: string } | string, + ...rest: [string?] + ): Promise<{}> { + let params: { appId: string; installationId: string }; + + if ( + paramsOrFirst && + typeof paramsOrFirst === 'object' && + !Array.isArray(paramsOrFirst) + ) { + params = (paramsOrFirst || {}) as { + appId: string; + installationId: string; + }; + } else { + params = { + appId: paramsOrFirst as string, + installationId: rest[0] as string, + }; + } + + const appId = params.appId; + const installationId = params.installationId; + + if (typeof appId === 'undefined' || appId === '') { + throw new AppwriteException('Missing required parameter: "appId"'); + } + + if (typeof installationId === 'undefined' || installationId === '') { + throw new AppwriteException( + 'Missing required parameter: "installationId"', + ); + } + + const apiPath = '/apps/{appId}/installations/{installationId}' + .replace('{appId}', encodeURIComponent(String(appId))) + .replace( + '{installationId}', + encodeURIComponent(String(installationId)), + ); + const apiPayload: Payload = {}; + + const uri = new URL(this.client.config.endpoint + apiPath); + return this.client.call( + 'delete', + uri, + { + 'X-Appwrite-Project': this.client.config.project, + 'content-type': 'application/json', + accept: 'application/json', + }, + apiPayload, + ); + } + + /** + * Create a token for an installation of an application. Requires an app key sent in the `X-Appwrite-Key` header alongside the `X-Appwrite-App` header, or a caller with update access to the app. The returned token carries the scopes and authorization details granted to the installation, and can be used as an `Authorization: Bearer` header everywhere OAuth2 access tokens are accepted. Multiple tokens can be active for the same installation at once; each token stays valid until it expires or the installation is updated or deleted. + * + * @param {string} params.appId - Application unique ID. + * @param {string} params.installationId - Installation unique ID. + * @throws {AppwriteException} + * @returns {Promise} + */ + createInstallationToken(params: { + appId: string; + installationId: string; + }): Promise; + /** + * Create a token for an installation of an application. Requires an app key sent in the `X-Appwrite-Key` header alongside the `X-Appwrite-App` header, or a caller with update access to the app. The returned token carries the scopes and authorization details granted to the installation, and can be used as an `Authorization: Bearer` header everywhere OAuth2 access tokens are accepted. Multiple tokens can be active for the same installation at once; each token stays valid until it expires or the installation is updated or deleted. + * + * @param {string} appId - Application unique ID. + * @param {string} installationId - Installation unique ID. + * @throws {AppwriteException} + * @returns {Promise} + * @deprecated Use the object parameter style method for a better developer experience. + */ + createInstallationToken( + appId: string, + installationId: string, + ): Promise; + createInstallationToken( + paramsOrFirst: { appId: string; installationId: string } | string, + ...rest: [string?] + ): Promise { + let params: { appId: string; installationId: string }; + + if ( + paramsOrFirst && + typeof paramsOrFirst === 'object' && + !Array.isArray(paramsOrFirst) + ) { + params = (paramsOrFirst || {}) as { + appId: string; + installationId: string; + }; + } else { + params = { + appId: paramsOrFirst as string, + installationId: rest[0] as string, + }; + } + + const appId = params.appId; + const installationId = params.installationId; + + if (typeof appId === 'undefined' || appId === '') { + throw new AppwriteException('Missing required parameter: "appId"'); + } + + if (typeof installationId === 'undefined' || installationId === '') { + throw new AppwriteException( + 'Missing required parameter: "installationId"', + ); + } + + const apiPath = '/apps/{appId}/installations/{installationId}/tokens' + .replace('{appId}', encodeURIComponent(String(appId))) + .replace( + '{installationId}', + encodeURIComponent(String(installationId)), + ); + const apiPayload: Payload = {}; + + const uri = new URL(this.client.config.endpoint + apiPath); + return this.client.call( + 'post', + uri, + { + 'X-Appwrite-Project': this.client.config.project, + 'content-type': 'application/json', + accept: 'application/json', + }, + apiPayload, + ); + } + + /** + * List app keys for an application. + * + * @param {string} params.appId - Application unique 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. + * @param {boolean} params.total - When set to false, the total count returned will be 0 and will not be calculated. + * @throws {AppwriteException} + * @returns {Promise} + */ + listKeys(params: { + appId: string; + queries?: string[]; + total?: boolean; + }): Promise; + /** + * List app keys for an application. + * + * @param {string} appId - Application unique 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. + * @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. + */ + listKeys( + appId: string, + queries?: string[], + total?: boolean, + ): Promise; + listKeys( + paramsOrFirst: + { appId: string; queries?: string[]; total?: boolean } | string, + ...rest: [string[]?, boolean?] + ): Promise { + let params: { appId: string; queries?: string[]; total?: boolean }; + + if ( + paramsOrFirst && + typeof paramsOrFirst === 'object' && + !Array.isArray(paramsOrFirst) + ) { + params = (paramsOrFirst || {}) as { + appId: string; + queries?: string[]; + total?: boolean; + }; + } else { + params = { + appId: paramsOrFirst as string, + queries: rest[0] as string[], + total: rest[1] as boolean, + }; + } + + const appId = params.appId; + const queries = params.queries; + const total = params.total; + + if (typeof appId === 'undefined' || appId === '') { + throw new AppwriteException('Missing required parameter: "appId"'); + } + + const apiPath = '/apps/{appId}/keys'.replace( + '{appId}', + encodeURIComponent(String(appId)), + ); + const apiPayload: Payload = {}; + + if (typeof queries !== 'undefined') { + apiPayload['queries'] = queries; + } + + if (typeof total !== 'undefined') { + apiPayload['total'] = total; + } + + const uri = new URL(this.client.config.endpoint + apiPath); + return this.client.call( + 'get', + uri, + { + 'X-Appwrite-Project': this.client.config.project, + accept: 'application/json', + }, + apiPayload, + ); + } + + /** + * Create a new app key for an application. App keys carry no scopes; send one in the `X-Appwrite-Key` header alongside the `X-Appwrite-App` header to list the application's installations and create installation access tokens. + * + * @param {string} params.appId - Application unique ID. + * @throws {AppwriteException} + * @returns {Promise} + */ + createKey(params: { appId: string }): Promise; + /** + * Create a new app key for an application. App keys carry no scopes; send one in the `X-Appwrite-Key` header alongside the `X-Appwrite-App` header to list the application's installations and create installation access tokens. + * + * @param {string} appId - Application unique ID. + * @throws {AppwriteException} + * @returns {Promise} + * @deprecated Use the object parameter style method for a better developer experience. + */ + createKey(appId: string): Promise; + createKey( + paramsOrFirst: { appId: string } | string, + ): Promise { + let params: { appId: string }; + + if ( + paramsOrFirst && + typeof paramsOrFirst === 'object' && + !Array.isArray(paramsOrFirst) + ) { + params = (paramsOrFirst || {}) as { appId: string }; + } else { + params = { + appId: paramsOrFirst as string, + }; + } + + const appId = params.appId; + + if (typeof appId === 'undefined' || appId === '') { + throw new AppwriteException('Missing required parameter: "appId"'); + } + + const apiPath = '/apps/{appId}/keys'.replace( + '{appId}', + encodeURIComponent(String(appId)), + ); + const apiPayload: Payload = {}; + + const uri = new URL(this.client.config.endpoint + apiPath); + return this.client.call( + 'post', + uri, + { + 'X-Appwrite-Project': this.client.config.project, + 'content-type': 'application/json', + accept: 'application/json', + }, + apiPayload, + ); + } + + /** + * Get an app key by its unique ID. + * + * @param {string} params.appId - Application unique ID. + * @param {string} params.keyId - App key unique ID. + * @throws {AppwriteException} + * @returns {Promise} + */ + getKey(params: { appId: string; keyId: string }): Promise; + /** + * Get an app key by its unique ID. + * + * @param {string} appId - Application unique ID. + * @param {string} keyId - App key unique ID. + * @throws {AppwriteException} + * @returns {Promise} + * @deprecated Use the object parameter style method for a better developer experience. + */ + getKey(appId: string, keyId: string): Promise; + getKey( + paramsOrFirst: { appId: string; keyId: string } | string, + ...rest: [string?] + ): Promise { + let params: { appId: string; keyId: string }; + + if ( + paramsOrFirst && + typeof paramsOrFirst === 'object' && + !Array.isArray(paramsOrFirst) + ) { + params = (paramsOrFirst || {}) as { appId: string; keyId: string }; + } else { + params = { + appId: paramsOrFirst as string, + keyId: rest[0] as string, + }; + } + + const appId = params.appId; + const keyId = params.keyId; + + if (typeof appId === 'undefined' || appId === '') { + throw new AppwriteException('Missing required parameter: "appId"'); + } + + if (typeof keyId === 'undefined' || keyId === '') { + throw new AppwriteException('Missing required parameter: "keyId"'); + } + + const apiPath = '/apps/{appId}/keys/{keyId}' + .replace('{appId}', encodeURIComponent(String(appId))) + .replace('{keyId}', encodeURIComponent(String(keyId))); + const apiPayload: Payload = {}; + + const uri = new URL(this.client.config.endpoint + apiPath); + return this.client.call( + 'get', + uri, + { + 'X-Appwrite-Project': this.client.config.project, + accept: 'application/json', + }, + apiPayload, + ); + } + + /** + * Delete an app key by its unique ID. + * + * @param {string} params.appId - Application unique ID. + * @param {string} params.keyId - App key unique ID. + * @throws {AppwriteException} + * @returns {Promise} + */ + deleteKey(params: { appId: string; keyId: string }): Promise<{}>; + /** + * Delete an app key by its unique ID. + * + * @param {string} appId - Application unique ID. + * @param {string} keyId - App key unique ID. + * @throws {AppwriteException} + * @returns {Promise<{}>} + * @deprecated Use the object parameter style method for a better developer experience. + */ + deleteKey(appId: string, keyId: string): Promise<{}>; + deleteKey( + paramsOrFirst: { appId: string; keyId: string } | string, + ...rest: [string?] + ): Promise<{}> { + let params: { appId: string; keyId: string }; + + if ( + paramsOrFirst && + typeof paramsOrFirst === 'object' && + !Array.isArray(paramsOrFirst) + ) { + params = (paramsOrFirst || {}) as { appId: string; keyId: string }; + } else { + params = { + appId: paramsOrFirst as string, + keyId: rest[0] as string, + }; + } + + const appId = params.appId; + const keyId = params.keyId; + + if (typeof appId === 'undefined' || appId === '') { + throw new AppwriteException('Missing required parameter: "appId"'); + } + + if (typeof keyId === 'undefined' || keyId === '') { + throw new AppwriteException('Missing required parameter: "keyId"'); + } + + const apiPath = '/apps/{appId}/keys/{keyId}' + .replace('{appId}', encodeURIComponent(String(appId))) + .replace('{keyId}', encodeURIComponent(String(keyId))); + const apiPayload: Payload = {}; + + const uri = new URL(this.client.config.endpoint + apiPath); + return this.client.call( + 'delete', + uri, + { + 'X-Appwrite-Project': this.client.config.project, + 'content-type': 'application/json', + accept: 'application/json', + }, + apiPayload, + ); + } + + /** + * List client secrets for an application. + * + * @param {string} params.appId - Application unique 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. + * @param {boolean} params.total - When set to false, the total count returned will be 0 and will not be calculated. + * @throws {AppwriteException} + * @returns {Promise} + */ + listSecrets(params: { + appId: string; + queries?: string[]; + total?: boolean; + }): Promise; + /** + * List client secrets for an application. + * + * @param {string} appId - Application unique 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. + * @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. + */ + listSecrets( + appId: string, + queries?: string[], + total?: boolean, + ): Promise; + listSecrets( + paramsOrFirst: + { appId: string; queries?: string[]; total?: boolean } | string, + ...rest: [string[]?, boolean?] + ): Promise { + let params: { appId: string; queries?: string[]; total?: boolean }; + + if ( + paramsOrFirst && + typeof paramsOrFirst === 'object' && + !Array.isArray(paramsOrFirst) + ) { + params = (paramsOrFirst || {}) as { + appId: string; + queries?: string[]; + total?: boolean; + }; + } else { + params = { + appId: paramsOrFirst as string, + queries: rest[0] as string[], + total: rest[1] as boolean, + }; + } + + const appId = params.appId; + const queries = params.queries; + const total = params.total; + + if (typeof appId === 'undefined' || appId === '') { + throw new AppwriteException('Missing required parameter: "appId"'); + } + + const apiPath = '/apps/{appId}/secrets'.replace( + '{appId}', + encodeURIComponent(String(appId)), + ); + const apiPayload: Payload = {}; + + if (typeof queries !== 'undefined') { + apiPayload['queries'] = queries; + } + + if (typeof total !== 'undefined') { + apiPayload['total'] = total; + } + + const uri = new URL(this.client.config.endpoint + apiPath); + return this.client.call( + 'get', + uri, + { + 'X-Appwrite-Project': this.client.config.project, + accept: 'application/json', + }, + apiPayload, + ); + } + + /** + * Create a new client secret for an application. + * + * @param {string} params.appId - Application unique ID. + * @throws {AppwriteException} + * @returns {Promise} + */ + createSecret(params: { appId: string }): Promise; + /** + * Create a new client secret for an application. + * + * @param {string} appId - Application unique ID. + * @throws {AppwriteException} + * @returns {Promise} + * @deprecated Use the object parameter style method for a better developer experience. + */ + createSecret(appId: string): Promise; + createSecret( + paramsOrFirst: { appId: string } | string, + ): Promise { + let params: { appId: string }; + + if ( + paramsOrFirst && + typeof paramsOrFirst === 'object' && + !Array.isArray(paramsOrFirst) + ) { + params = (paramsOrFirst || {}) as { appId: string }; + } else { + params = { + appId: paramsOrFirst as string, + }; + } + + const appId = params.appId; + + if (typeof appId === 'undefined' || appId === '') { + throw new AppwriteException('Missing required parameter: "appId"'); + } + + const apiPath = '/apps/{appId}/secrets'.replace( + '{appId}', + encodeURIComponent(String(appId)), + ); + const apiPayload: Payload = {}; + + const uri = new URL(this.client.config.endpoint + apiPath); + return this.client.call( + 'post', + uri, + { + 'X-Appwrite-Project': this.client.config.project, + 'content-type': 'application/json', + accept: 'application/json', + }, + apiPayload, + ); + } + + /** + * Get an application client secret by its unique ID. + * + * @param {string} params.appId - Application unique ID. + * @param {string} params.secretId - Secret unique ID. + * @throws {AppwriteException} + * @returns {Promise} + */ + getSecret(params: { + appId: string; + secretId: string; + }): Promise; + /** + * Get an application client secret by its unique ID. + * + * @param {string} appId - Application unique ID. + * @param {string} secretId - Secret unique ID. + * @throws {AppwriteException} + * @returns {Promise} + * @deprecated Use the object parameter style method for a better developer experience. + */ + getSecret(appId: string, secretId: string): Promise; + getSecret( + paramsOrFirst: { appId: string; secretId: string } | string, + ...rest: [string?] + ): Promise { + let params: { appId: string; secretId: string }; + + if ( + paramsOrFirst && + typeof paramsOrFirst === 'object' && + !Array.isArray(paramsOrFirst) + ) { + params = (paramsOrFirst || {}) as { + appId: string; + secretId: string; + }; + } else { + params = { + appId: paramsOrFirst as string, + secretId: rest[0] as string, + }; + } + + const appId = params.appId; + const secretId = params.secretId; + + if (typeof appId === 'undefined' || appId === '') { + throw new AppwriteException('Missing required parameter: "appId"'); + } + + if (typeof secretId === 'undefined' || secretId === '') { + throw new AppwriteException( + 'Missing required parameter: "secretId"', + ); + } + + const apiPath = '/apps/{appId}/secrets/{secretId}' + .replace('{appId}', encodeURIComponent(String(appId))) + .replace('{secretId}', encodeURIComponent(String(secretId))); + const apiPayload: Payload = {}; + + const uri = new URL(this.client.config.endpoint + apiPath); + return this.client.call( + 'get', + uri, + { + 'X-Appwrite-Project': this.client.config.project, + accept: 'application/json', + }, + apiPayload, + ); + } + + /** + * Delete an application client secret by its unique ID. + * + * @param {string} params.appId - Application unique ID. + * @param {string} params.secretId - Secret unique ID. + * @throws {AppwriteException} + * @returns {Promise} + */ + deleteSecret(params: { appId: string; secretId: string }): Promise<{}>; + /** + * Delete an application client secret by its unique ID. + * + * @param {string} appId - Application unique ID. + * @param {string} secretId - Secret unique ID. + * @throws {AppwriteException} + * @returns {Promise<{}>} + * @deprecated Use the object parameter style method for a better developer experience. + */ + deleteSecret(appId: string, secretId: string): Promise<{}>; + deleteSecret( + paramsOrFirst: { appId: string; secretId: string } | string, + ...rest: [string?] + ): Promise<{}> { + let params: { appId: string; secretId: string }; + + if ( + paramsOrFirst && + typeof paramsOrFirst === 'object' && + !Array.isArray(paramsOrFirst) + ) { + params = (paramsOrFirst || {}) as { + appId: string; + secretId: string; + }; + } else { + params = { + appId: paramsOrFirst as string, + secretId: rest[0] as string, + }; + } + + const appId = params.appId; + const secretId = params.secretId; + + if (typeof appId === 'undefined' || appId === '') { + throw new AppwriteException('Missing required parameter: "appId"'); + } + + if (typeof secretId === 'undefined' || secretId === '') { + throw new AppwriteException( + 'Missing required parameter: "secretId"', + ); + } + + const apiPath = '/apps/{appId}/secrets/{secretId}' + .replace('{appId}', encodeURIComponent(String(appId))) + .replace('{secretId}', encodeURIComponent(String(secretId))); + const apiPayload: Payload = {}; + + const uri = new URL(this.client.config.endpoint + apiPath); + return this.client.call( + 'delete', + uri, + { + 'X-Appwrite-Project': this.client.config.project, + 'content-type': 'application/json', + accept: 'application/json', + }, + apiPayload, + ); + } + + /** + * Transfer an application to another team by its unique ID. + * + * @param {string} params.appId - Application unique ID. + * @param {string} params.teamId - Team ID of the team to transfer application to. + * @throws {AppwriteException} + * @returns {Promise} + */ + updateTeam(params: { appId: string; teamId: string }): Promise; + /** + * Transfer an application to another team by its unique ID. + * + * @param {string} appId - Application unique ID. + * @param {string} teamId - Team ID of the team to transfer application to. + * @throws {AppwriteException} + * @returns {Promise} + * @deprecated Use the object parameter style method for a better developer experience. + */ + updateTeam(appId: string, teamId: string): Promise; + updateTeam( + paramsOrFirst: { appId: string; teamId: string } | string, + ...rest: [string?] + ): Promise { + let params: { appId: string; teamId: string }; + + if ( + paramsOrFirst && + typeof paramsOrFirst === 'object' && + !Array.isArray(paramsOrFirst) + ) { + params = (paramsOrFirst || {}) as { appId: string; teamId: string }; + } else { + params = { + appId: paramsOrFirst as string, + teamId: rest[0] as string, + }; + } + + const appId = params.appId; + const teamId = params.teamId; + + if (typeof appId === 'undefined' || appId === '') { + throw new AppwriteException('Missing required parameter: "appId"'); + } + + if (typeof teamId === 'undefined') { + throw new AppwriteException('Missing required parameter: "teamId"'); + } + + const apiPath = '/apps/{appId}/team'.replace( + '{appId}', + encodeURIComponent(String(appId)), + ); + const apiPayload: Payload = {}; + + if (typeof teamId !== 'undefined') { + apiPayload['teamId'] = teamId; + } + + const uri = new URL(this.client.config.endpoint + apiPath); + return this.client.call( + 'patch', + uri, + { + 'X-Appwrite-Project': this.client.config.project, + 'content-type': 'application/json', + accept: 'application/json', + }, + apiPayload, + ); + } + + /** + * Revoke all tokens for an application by its unique ID. + * + * @param {string} params.appId - Application unique ID. + * @throws {AppwriteException} + * @returns {Promise} + */ + deleteTokens(params: { appId: string }): Promise<{}>; + /** + * Revoke all tokens for an application by its unique ID. + * + * @param {string} appId - Application unique ID. + * @throws {AppwriteException} + * @returns {Promise<{}>} + * @deprecated Use the object parameter style method for a better developer experience. + */ + deleteTokens(appId: string): Promise<{}>; + deleteTokens(paramsOrFirst: { appId: string } | string): Promise<{}> { + let params: { appId: string }; + + if ( + paramsOrFirst && + typeof paramsOrFirst === 'object' && + !Array.isArray(paramsOrFirst) + ) { + params = (paramsOrFirst || {}) as { appId: string }; + } else { + params = { + appId: paramsOrFirst as string, + }; + } + + const appId = params.appId; + + if (typeof appId === 'undefined' || appId === '') { + throw new AppwriteException('Missing required parameter: "appId"'); + } + + const apiPath = '/apps/{appId}/tokens'.replace( + '{appId}', + encodeURIComponent(String(appId)), + ); + const apiPayload: Payload = {}; + + const uri = new URL(this.client.config.endpoint + apiPath); + return this.client.call( + 'delete', + uri, + { + 'X-Appwrite-Project': this.client.config.project, + 'content-type': 'application/json', + accept: 'application/json', + }, + apiPayload, + ); + } +} diff --git a/src/services/avatars.ts b/src/services/avatars.ts index 68c702e8..e634ce6d 100644 --- a/src/services/avatars.ts +++ b/src/services/avatars.ts @@ -1,6 +1,9 @@ import { Service } from '../service'; import { AppwriteException, Client } from '../client'; -import type { Payload } 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'; import { Browser } from '../enums/browser'; import { CreditCard } from '../enums/credit-card'; @@ -106,26 +109,28 @@ export class Avatars extends Service { '{code}', encodeURIComponent(String(code)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof width !== 'undefined') { - payload['width'] = width; + apiPayload['width'] = width; } if (typeof height !== 'undefined') { - payload['height'] = height; + apiPayload['height'] = height; } if (typeof quality !== 'undefined') { - payload['quality'] = quality; + apiPayload['quality'] = quality; } const uri = new URL(this.client.config.endpoint + apiPath); - payload['project'] = this.client.config.project; + apiPayload['project'] = this.client.config.project; - payload['impersonateuserid'] = this.client.config.impersonateuserid; + apiPayload['impersonateuserid'] = this.client.config.impersonateuserid; - for (const [key, value] of Object.entries(Service.flatten(payload))) { + for (const [key, value] of Object.entries( + Service.flatten(apiPayload), + )) { uri.searchParams.append(key, value); } return this.client.call( @@ -135,7 +140,7 @@ export class Avatars extends Service { 'X-Appwrite-Project': this.client.config.project, accept: 'image/png', }, - payload, + apiPayload, 'arrayBuffer', ); } @@ -234,26 +239,28 @@ export class Avatars extends Service { '{code}', encodeURIComponent(String(code)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof width !== 'undefined') { - payload['width'] = width; + apiPayload['width'] = width; } if (typeof height !== 'undefined') { - payload['height'] = height; + apiPayload['height'] = height; } if (typeof quality !== 'undefined') { - payload['quality'] = quality; + apiPayload['quality'] = quality; } const uri = new URL(this.client.config.endpoint + apiPath); - payload['project'] = this.client.config.project; + apiPayload['project'] = this.client.config.project; - payload['impersonateuserid'] = this.client.config.impersonateuserid; + apiPayload['impersonateuserid'] = this.client.config.impersonateuserid; - for (const [key, value] of Object.entries(Service.flatten(payload))) { + for (const [key, value] of Object.entries( + Service.flatten(apiPayload), + )) { uri.searchParams.append(key, value); } return this.client.call( @@ -263,7 +270,7 @@ export class Avatars extends Service { 'X-Appwrite-Project': this.client.config.project, accept: 'image/png', }, - payload, + apiPayload, 'arrayBuffer', ); } @@ -311,18 +318,20 @@ export class Avatars extends Service { } const apiPath = '/avatars/favicon'; - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof url !== 'undefined') { - payload['url'] = url; + apiPayload['url'] = url; } const uri = new URL(this.client.config.endpoint + apiPath); - payload['project'] = this.client.config.project; + apiPayload['project'] = this.client.config.project; - payload['impersonateuserid'] = this.client.config.impersonateuserid; + apiPayload['impersonateuserid'] = this.client.config.impersonateuserid; - for (const [key, value] of Object.entries(Service.flatten(payload))) { + for (const [key, value] of Object.entries( + Service.flatten(apiPayload), + )) { uri.searchParams.append(key, value); } return this.client.call( @@ -332,7 +341,7 @@ export class Avatars extends Service { 'X-Appwrite-Project': this.client.config.project, accept: 'image/*', }, - payload, + apiPayload, 'arrayBuffer', ); } @@ -426,26 +435,28 @@ export class Avatars extends Service { '{code}', encodeURIComponent(String(code)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof width !== 'undefined') { - payload['width'] = width; + apiPayload['width'] = width; } if (typeof height !== 'undefined') { - payload['height'] = height; + apiPayload['height'] = height; } if (typeof quality !== 'undefined') { - payload['quality'] = quality; + apiPayload['quality'] = quality; } const uri = new URL(this.client.config.endpoint + apiPath); - payload['project'] = this.client.config.project; + apiPayload['project'] = this.client.config.project; - payload['impersonateuserid'] = this.client.config.impersonateuserid; + apiPayload['impersonateuserid'] = this.client.config.impersonateuserid; - for (const [key, value] of Object.entries(Service.flatten(payload))) { + for (const [key, value] of Object.entries( + Service.flatten(apiPayload), + )) { uri.searchParams.append(key, value); } return this.client.call( @@ -455,7 +466,7 @@ export class Avatars extends Service { 'X-Appwrite-Project': this.client.config.project, accept: 'image/png', }, - payload, + apiPayload, 'arrayBuffer', ); } @@ -531,26 +542,28 @@ export class Avatars extends Service { } const apiPath = '/avatars/image'; - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof url !== 'undefined') { - payload['url'] = url; + apiPayload['url'] = url; } if (typeof width !== 'undefined') { - payload['width'] = width; + apiPayload['width'] = width; } if (typeof height !== 'undefined') { - payload['height'] = height; + apiPayload['height'] = height; } const uri = new URL(this.client.config.endpoint + apiPath); - payload['project'] = this.client.config.project; + apiPayload['project'] = this.client.config.project; - payload['impersonateuserid'] = this.client.config.impersonateuserid; + apiPayload['impersonateuserid'] = this.client.config.impersonateuserid; - for (const [key, value] of Object.entries(Service.flatten(payload))) { + for (const [key, value] of Object.entries( + Service.flatten(apiPayload), + )) { uri.searchParams.append(key, value); } return this.client.call( @@ -560,7 +573,7 @@ export class Avatars extends Service { 'X-Appwrite-Project': this.client.config.project, accept: 'image/*', }, - payload, + apiPayload, 'arrayBuffer', ); } @@ -653,30 +666,32 @@ export class Avatars extends Service { const background = params.background; const apiPath = '/avatars/initials'; - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof name !== 'undefined') { - payload['name'] = name; + apiPayload['name'] = name; } if (typeof width !== 'undefined') { - payload['width'] = width; + apiPayload['width'] = width; } if (typeof height !== 'undefined') { - payload['height'] = height; + apiPayload['height'] = height; } if (typeof background !== 'undefined') { - payload['background'] = background; + apiPayload['background'] = background; } const uri = new URL(this.client.config.endpoint + apiPath); - payload['project'] = this.client.config.project; + apiPayload['project'] = this.client.config.project; - payload['impersonateuserid'] = this.client.config.impersonateuserid; + apiPayload['impersonateuserid'] = this.client.config.impersonateuserid; - for (const [key, value] of Object.entries(Service.flatten(payload))) { + for (const [key, value] of Object.entries( + Service.flatten(apiPayload), + )) { uri.searchParams.append(key, value); } return this.client.call( @@ -686,13 +701,13 @@ export class Avatars extends Service { 'X-Appwrite-Project': this.client.config.project, accept: 'image/png', }, - payload, + apiPayload, 'arrayBuffer', ); } /** - * Returns the best available profile photo for a user. The endpoint tries each source in priority order and returns the first successful result: OAuth2 identity photo, Gravatar, Libravatar, Appwrite Initials, built-in static fallback. + * Returns the best available profile photo for a user. The endpoint tries each source in priority order and returns the first successful result: a custom uploaded photo (see avatars.updatePhoto), OAuth2 identity photo, Gravatar, Libravatar, Appwrite Initials, built-in static fallback. * * Passing `userId` — `current()` for the authenticated user — resolves the photo from everything known about that user: identity photos, email, and name. An explicit `emailHash` or `name` then overrides just that value, and the user's remaining sources stay in the chain. Without `userId`, passing `emailHash` and/or `name` resolves the avatar from those values alone: the hash is looked up on Gravatar and Libravatar, the name is rendered as initials, and the session user stays out of the chain so their own photo never shadows the avatar being asked for. When nothing is passed, the photo resolves for the currently authenticated user. Emails are only ever accepted pre-hashed, so no address ends up in a URL. * @@ -718,7 +733,7 @@ export class Avatars extends Service { name?: string; }): Promise; /** - * Returns the best available profile photo for a user. The endpoint tries each source in priority order and returns the first successful result: OAuth2 identity photo, Gravatar, Libravatar, Appwrite Initials, built-in static fallback. + * Returns the best available profile photo for a user. The endpoint tries each source in priority order and returns the first successful result: a custom uploaded photo (see avatars.updatePhoto), OAuth2 identity photo, Gravatar, Libravatar, Appwrite Initials, built-in static fallback. * * Passing `userId` — `current()` for the authenticated user — resolves the photo from everything known about that user: identity photos, email, and name. An explicit `emailHash` or `name` then overrides just that value, and the user's remaining sources stay in the chain. Without `userId`, passing `emailHash` and/or `name` resolves the avatar from those values alone: the hash is looked up on Gravatar and Libravatar, the name is rendered as initials, and the session user stays out of the chain so their own photo never shadows the avatar being asked for. When nothing is passed, the photo resolves for the currently authenticated user. Emails are only ever accepted pre-hashed, so no address ends up in a URL. * @@ -809,46 +824,48 @@ export class Avatars extends Service { const name = params.name; const apiPath = '/avatars/photo'; - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof width !== 'undefined') { - payload['width'] = width; + apiPayload['width'] = width; } if (typeof height !== 'undefined') { - payload['height'] = height; + apiPayload['height'] = height; } if (typeof quality !== 'undefined') { - payload['quality'] = quality; + apiPayload['quality'] = quality; } if (typeof output !== 'undefined') { - payload['output'] = output; + apiPayload['output'] = output; } if (typeof rating !== 'undefined') { - payload['rating'] = rating; + apiPayload['rating'] = rating; } if (typeof userId !== 'undefined') { - payload['userId'] = userId; + apiPayload['userId'] = userId; } if (typeof emailHash !== 'undefined') { - payload['emailHash'] = emailHash; + apiPayload['emailHash'] = emailHash; } if (typeof name !== 'undefined') { - payload['name'] = name; + apiPayload['name'] = name; } const uri = new URL(this.client.config.endpoint + apiPath); - payload['project'] = this.client.config.project; + apiPayload['project'] = this.client.config.project; - payload['impersonateuserid'] = this.client.config.impersonateuserid; + apiPayload['impersonateuserid'] = this.client.config.impersonateuserid; - for (const [key, value] of Object.entries(Service.flatten(payload))) { + for (const [key, value] of Object.entries( + Service.flatten(apiPayload), + )) { uri.searchParams.append(key, value); } return this.client.call( @@ -858,11 +875,321 @@ export class Avatars extends Service { 'X-Appwrite-Project': this.client.config.project, accept: 'image/*', }, - payload, + apiPayload, 'arrayBuffer', ); } + /** + * Update the profile photo of the currently authenticated user. The uploaded image takes priority over every other photo source, including OAuth2 identity photos, Gravatar, and Libravatar. Updating an already customized photo replaces it. The image must be at most 5MB and is sent in a single request. + * + * @param {{ name: string; type: string; size: number; uri: string }} params.file - Binary image file of at most 5MB. Allowed file types are png, jpg, jpeg, and webp. + * @throws {AppwriteException} + * @returns {Promise} + */ + async updatePhoto< + Preferences extends Models.Preferences = Models.DefaultPreferences, + >(params: { + file: { name: string; type: string; size: number; uri: string }; + onProgress?: (progress: UploadProgress) => void; + }): Promise>; + /** + * Update the profile photo of the currently authenticated user. The uploaded image takes priority over every other photo source, including OAuth2 identity photos, Gravatar, and Libravatar. Updating an already customized photo replaces it. The image must be at most 5MB and is sent in a single request. + * + * @param {{ name: string; type: string; size: number; uri: string }} file - Binary image file of at most 5MB. Allowed file types are png, jpg, jpeg, and webp. + * @throws {AppwriteException} + * @returns {Promise>} + * @deprecated Use the object parameter style method for a better developer experience. + */ + async updatePhoto< + Preferences extends Models.Preferences = Models.DefaultPreferences, + >( + file: { name: string; type: string; size: number; uri: string }, + onProgress?: (progress: UploadProgress) => void, + ): Promise>; + async updatePhoto< + Preferences extends Models.Preferences = Models.DefaultPreferences, + >( + paramsOrFirst: + | { + file: { + name: string; + type: string; + size: number; + uri: string; + }; + onProgress?: (progress: UploadProgress) => void; + } + | { name: string; type: string; size: number; uri: string }, + ...rest: [((progress: UploadProgress) => void)?] + ): Promise> { + let params: { + file: { name: string; type: string; size: number; uri: string }; + }; + let onProgress: (progress: UploadProgress) => void; + + if ( + paramsOrFirst && + typeof paramsOrFirst === 'object' && + !Array.isArray(paramsOrFirst) && + ('file' in paramsOrFirst || 'onProgress' in paramsOrFirst) + ) { + params = (paramsOrFirst || {}) as { + file: { name: string; type: string; size: number; uri: string }; + }; + onProgress = paramsOrFirst?.onProgress as ( + progress: UploadProgress, + ) => void; + } else { + params = { + file: paramsOrFirst as { + name: string; + type: string; + size: number; + uri: string; + }, + }; + onProgress = rest[0] as (progress: UploadProgress) => void; + } + + const file = params.file; + + if (typeof file === 'undefined') { + throw new AppwriteException('Missing required parameter: "file"'); + } + + const apiPath = '/avatars/photo'; + const apiPayload: Payload = {}; + + if (typeof file !== 'undefined') { + apiPayload['file'] = file; + } + + const uri = new URL(this.client.config.endpoint + apiPath); + + const apiHeaders: { [header: string]: string } = { + 'X-Appwrite-Project': this.client.config.project, + 'content-type': 'multipart/form-data', + accept: 'application/json', + }; + + const size = file.size; + + if (size <= Service.CHUNK_SIZE) { + apiPayload['file'] = Service.filePart( + file.uri, + file.name, + file.type, + () => + FileSystem.readAsStringAsync(file.uri, { + encoding: FileSystem.EncodingType.Base64, + }), + ); + + return this.client.call('put', uri, apiHeaders, apiPayload); + } + + let offset = 0; + let response = undefined; + + const totalChunks = Math.ceil(size / Service.CHUNK_SIZE); + + // Upload first chunk alone to get the upload ID + if (offset === 0) { + const firstChunkEnd = Math.min(Service.CHUNK_SIZE, size); + const firstChunkHeaders = { + ...apiHeaders, + 'content-range': 'bytes 0-' + (firstChunkEnd - 1) + '/' + size, + }; + + const firstChunk = await FileSystem.readAsStringAsync(file.uri, { + encoding: FileSystem.EncodingType.Base64, + position: 0, + length: Service.CHUNK_SIZE, + }); + let firstPath = `data:${file.type};base64,${firstChunk}`; + if (RNPlatform.OS.toLowerCase() === 'android') { + firstPath = + FileSystem.cacheDirectory + + '/tmp_chunk_' + + new Date().getTime(); + await FileSystem.writeAsStringAsync(firstPath, firstChunk, { + encoding: FileSystem.EncodingType.Base64, + }); + } + + apiPayload['file'] = Service.filePart( + firstPath, + file.name, + file.type, + async () => firstChunk, + ); + + response = await this.client.call( + 'put', + uri, + firstChunkHeaders, + apiPayload, + ); + offset = firstChunkEnd; + + if (onProgress) { + onProgress({ + $id: response.$id, + progress: (offset / size) * 100, + sizeUploaded: offset, + chunksTotal: totalChunks, + chunksUploaded: 1, + }); + } + } + + if (offset >= size) { + return response; + } + + const uploadId = response?.$id; + const chunks: { index: number; start: number; end: number }[] = []; + const startChunkIndex = Math.ceil(offset / Service.CHUNK_SIZE); + for (let i = startChunkIndex; i < totalChunks; i++) { + const start = i * Service.CHUNK_SIZE; + const end = Math.min(start + Service.CHUNK_SIZE, size); + chunks.push({ index: i, start, end }); + } + + // Upload remaining chunks with max concurrency of 8 + const CONCURRENCY = 8; + let completedCount = startChunkIndex; + let uploadedBytes = offset; + let finalResponse = null; + let failed = false; + + const isUploadComplete = (chunkResponse: any) => { + const chunksUploaded = chunkResponse?.chunksUploaded; + const chunksTotal = chunkResponse?.chunksTotal ?? totalChunks; + return ( + typeof chunksUploaded === 'number' && + typeof chunksTotal === 'number' && + chunksUploaded >= chunksTotal + ); + }; + + const uploadChunk = async (chunk: (typeof chunks)[0]) => { + const chunkHeaders = { ...apiHeaders }; + if (uploadId) { + chunkHeaders['x-appwrite-id'] = uploadId; + } + chunkHeaders['content-range'] = + 'bytes ' + chunk.start + '-' + (chunk.end - 1) + '/' + size; + + const chunkData = await FileSystem.readAsStringAsync(file.uri, { + encoding: FileSystem.EncodingType.Base64, + position: chunk.start, + length: chunk.end - chunk.start, + }); + + let chunkPath = `data:${file.type};base64,${chunkData}`; + if (RNPlatform.OS.toLowerCase() === 'android') { + chunkPath = + FileSystem.cacheDirectory + + '/tmp_chunk_' + + new Date().getTime() + + '_' + + chunk.index; + await FileSystem.writeAsStringAsync(chunkPath, chunkData, { + encoding: FileSystem.EncodingType.Base64, + }); + } + + const chunkPayload = { ...apiPayload }; + chunkPayload['file'] = Service.filePart( + chunkPath, + file.name, + file.type, + async () => chunkData, + ); + + const chunkResponse = await this.client.call( + 'put', + uri, + chunkHeaders, + chunkPayload, + ); + + if (failed) { + return chunkResponse; + } + + completedCount++; + uploadedBytes += chunk.end - chunk.start; + + response = chunkResponse; + if (isUploadComplete(chunkResponse)) { + finalResponse = chunkResponse; + } + + if (onProgress) { + onProgress({ + $id: uploadId, + progress: (uploadedBytes / size) * 100, + sizeUploaded: uploadedBytes, + chunksTotal: totalChunks, + chunksUploaded: completedCount, + }); + } + + return chunkResponse; + }; + + // Process with limited concurrency using a worker pool + const queue = [...chunks]; + const workers: Promise[] = []; + const workerCount = Math.min(CONCURRENCY, queue.length); + + for (let i = 0; i < workerCount; i++) { + workers.push( + (async () => { + while (!failed && queue.length > 0) { + const chunk = queue.shift()!; + try { + await uploadChunk(chunk); + } catch (error) { + failed = true; + throw error; + } + } + })(), + ); + } + + await Promise.all(workers); + + return finalResponse ?? response; + } + + /** + * Delete the profile photo of the currently authenticated user and store the built-in static placeholder in its place. The placeholder is the user's photo from then on, so it takes priority over every other photo source — OAuth2 identity photos, Gravatar, Libravatar, and initials — until a new photo is uploaded with avatars.updatePhoto. + * + * @throws {AppwriteException} + * @returns {Promise} + */ + deletePhoto(): Promise<{}> { + const apiPath = '/avatars/photo'; + const apiPayload: Payload = {}; + + const uri = new URL(this.client.config.endpoint + apiPath); + return this.client.call( + 'delete', + uri, + { + 'X-Appwrite-Project': this.client.config.project, + 'content-type': 'application/json', + accept: 'application/json', + }, + apiPayload, + ); + } + /** * Converts a given plain text to a QR code image. You can use the query parameters to change the size and style of the resulting image. * @@ -946,30 +1273,32 @@ export class Avatars extends Service { } const apiPath = '/avatars/qr'; - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof text !== 'undefined') { - payload['text'] = text; + apiPayload['text'] = text; } if (typeof size !== 'undefined') { - payload['size'] = size; + apiPayload['size'] = size; } if (typeof margin !== 'undefined') { - payload['margin'] = margin; + apiPayload['margin'] = margin; } if (typeof download !== 'undefined') { - payload['download'] = download; + apiPayload['download'] = download; } const uri = new URL(this.client.config.endpoint + apiPath); - payload['project'] = this.client.config.project; + apiPayload['project'] = this.client.config.project; - payload['impersonateuserid'] = this.client.config.impersonateuserid; + apiPayload['impersonateuserid'] = this.client.config.impersonateuserid; - for (const [key, value] of Object.entries(Service.flatten(payload))) { + for (const [key, value] of Object.entries( + Service.flatten(apiPayload), + )) { uri.searchParams.append(key, value); } return this.client.call( @@ -979,7 +1308,7 @@ export class Avatars extends Service { 'X-Appwrite-Project': this.client.config.project, accept: 'image/png', }, - payload, + apiPayload, 'arrayBuffer', ); } @@ -992,7 +1321,7 @@ export class Avatars extends Service { * When width and height are specified, the image is resized accordingly. If both dimensions are 0, the API provides an image at original size. If dimensions are not specified, the default viewport size is 1280x720px. * * @param {string} params.url - Website URL which you want to capture. - * @param {object} params.headers - HTTP headers to send with the browser request. Defaults to empty. + * @param {object} params.headers - HTTP headers to send with the browser request. Only Accept and Accept-Language are allowed. Defaults to empty. * @param {number} params.viewportWidth - Browser viewport width. Pass an integer between 1 to 1920. Defaults to 1280. * @param {number} params.viewportHeight - Browser viewport height. Pass an integer between 1 to 1080. Defaults to 720. * @param {number} params.scale - Browser scale factor. Pass a number between 0.1 to 3. Defaults to 1. @@ -1044,7 +1373,7 @@ export class Avatars extends Service { * When width and height are specified, the image is resized accordingly. If both dimensions are 0, the API provides an image at original size. If dimensions are not specified, the default viewport size is 1280x720px. * * @param {string} url - Website URL which you want to capture. - * @param {object} headers - HTTP headers to send with the browser request. Defaults to empty. + * @param {object} headers - HTTP headers to send with the browser request. Only Accept and Accept-Language are allowed. Defaults to empty. * @param {number} viewportWidth - Browser viewport width. Pass an integer between 1 to 1920. Defaults to 1280. * @param {number} viewportHeight - Browser viewport height. Pass an integer between 1 to 1080. Defaults to 720. * @param {number} scale - Browser scale factor. Pass a number between 0.1 to 3. Defaults to 1. @@ -1237,94 +1566,96 @@ export class Avatars extends Service { } const apiPath = '/avatars/screenshots'; - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof url !== 'undefined') { - payload['url'] = url; + apiPayload['url'] = url; } if (typeof headers !== 'undefined') { - payload['headers'] = headers; + apiPayload['headers'] = headers; } if (typeof viewportWidth !== 'undefined') { - payload['viewportWidth'] = viewportWidth; + apiPayload['viewportWidth'] = viewportWidth; } if (typeof viewportHeight !== 'undefined') { - payload['viewportHeight'] = viewportHeight; + apiPayload['viewportHeight'] = viewportHeight; } if (typeof scale !== 'undefined') { - payload['scale'] = scale; + apiPayload['scale'] = scale; } if (typeof theme !== 'undefined') { - payload['theme'] = theme; + apiPayload['theme'] = theme; } if (typeof userAgent !== 'undefined') { - payload['userAgent'] = userAgent; + apiPayload['userAgent'] = userAgent; } if (typeof fullpage !== 'undefined') { - payload['fullpage'] = fullpage; + apiPayload['fullpage'] = fullpage; } if (typeof locale !== 'undefined') { - payload['locale'] = locale; + apiPayload['locale'] = locale; } if (typeof timezone !== 'undefined') { - payload['timezone'] = timezone; + apiPayload['timezone'] = timezone; } if (typeof latitude !== 'undefined') { - payload['latitude'] = latitude; + apiPayload['latitude'] = latitude; } if (typeof longitude !== 'undefined') { - payload['longitude'] = longitude; + apiPayload['longitude'] = longitude; } if (typeof accuracy !== 'undefined') { - payload['accuracy'] = accuracy; + apiPayload['accuracy'] = accuracy; } if (typeof touch !== 'undefined') { - payload['touch'] = touch; + apiPayload['touch'] = touch; } if (typeof permissions !== 'undefined') { - payload['permissions'] = permissions; + apiPayload['permissions'] = permissions; } if (typeof sleep !== 'undefined') { - payload['sleep'] = sleep; + apiPayload['sleep'] = sleep; } if (typeof width !== 'undefined') { - payload['width'] = width; + apiPayload['width'] = width; } if (typeof height !== 'undefined') { - payload['height'] = height; + apiPayload['height'] = height; } if (typeof quality !== 'undefined') { - payload['quality'] = quality; + apiPayload['quality'] = quality; } if (typeof output !== 'undefined') { - payload['output'] = output; + apiPayload['output'] = output; } const uri = new URL(this.client.config.endpoint + apiPath); - payload['project'] = this.client.config.project; + apiPayload['project'] = this.client.config.project; - payload['impersonateuserid'] = this.client.config.impersonateuserid; + apiPayload['impersonateuserid'] = this.client.config.impersonateuserid; - for (const [key, value] of Object.entries(Service.flatten(payload))) { + for (const [key, value] of Object.entries( + Service.flatten(apiPayload), + )) { uri.searchParams.append(key, value); } return this.client.call( @@ -1334,7 +1665,7 @@ export class Avatars extends Service { 'X-Appwrite-Project': this.client.config.project, accept: 'image/png', }, - payload, + apiPayload, 'arrayBuffer', ); } @@ -1372,26 +1703,28 @@ export class Avatars extends Service { '{code}', encodeURIComponent(String(code)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof width !== 'undefined') { - payload['width'] = width; + apiPayload['width'] = width; } if (typeof height !== 'undefined') { - payload['height'] = height; + apiPayload['height'] = height; } if (typeof quality !== 'undefined') { - payload['quality'] = quality; + apiPayload['quality'] = quality; } const uri = new URL(this.client.config.endpoint + apiPath); - payload['project'] = this.client.config.project; + apiPayload['project'] = this.client.config.project; - payload['impersonateuserid'] = this.client.config.impersonateuserid; + apiPayload['impersonateuserid'] = this.client.config.impersonateuserid; - for (const [key, value] of Object.entries(Service.flatten(payload))) { + for (const [key, value] of Object.entries( + Service.flatten(apiPayload), + )) { uri.searchParams.append(key, value); } @@ -1430,26 +1763,28 @@ export class Avatars extends Service { '{code}', encodeURIComponent(String(code)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof width !== 'undefined') { - payload['width'] = width; + apiPayload['width'] = width; } if (typeof height !== 'undefined') { - payload['height'] = height; + apiPayload['height'] = height; } if (typeof quality !== 'undefined') { - payload['quality'] = quality; + apiPayload['quality'] = quality; } const uri = new URL(this.client.config.endpoint + apiPath); - payload['project'] = this.client.config.project; + apiPayload['project'] = this.client.config.project; - payload['impersonateuserid'] = this.client.config.impersonateuserid; + apiPayload['impersonateuserid'] = this.client.config.impersonateuserid; - for (const [key, value] of Object.entries(Service.flatten(payload))) { + for (const [key, value] of Object.entries( + Service.flatten(apiPayload), + )) { uri.searchParams.append(key, value); } @@ -1472,18 +1807,20 @@ export class Avatars extends Service { } const apiPath = '/avatars/favicon'; - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof url !== 'undefined') { - payload['url'] = url; + apiPayload['url'] = url; } const uri = new URL(this.client.config.endpoint + apiPath); - payload['project'] = this.client.config.project; + apiPayload['project'] = this.client.config.project; - payload['impersonateuserid'] = this.client.config.impersonateuserid; + apiPayload['impersonateuserid'] = this.client.config.impersonateuserid; - for (const [key, value] of Object.entries(Service.flatten(payload))) { + for (const [key, value] of Object.entries( + Service.flatten(apiPayload), + )) { uri.searchParams.append(key, value); } @@ -1523,26 +1860,28 @@ export class Avatars extends Service { '{code}', encodeURIComponent(String(code)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof width !== 'undefined') { - payload['width'] = width; + apiPayload['width'] = width; } if (typeof height !== 'undefined') { - payload['height'] = height; + apiPayload['height'] = height; } if (typeof quality !== 'undefined') { - payload['quality'] = quality; + apiPayload['quality'] = quality; } const uri = new URL(this.client.config.endpoint + apiPath); - payload['project'] = this.client.config.project; + apiPayload['project'] = this.client.config.project; - payload['impersonateuserid'] = this.client.config.impersonateuserid; + apiPayload['impersonateuserid'] = this.client.config.impersonateuserid; - for (const [key, value] of Object.entries(Service.flatten(payload))) { + for (const [key, value] of Object.entries( + Service.flatten(apiPayload), + )) { uri.searchParams.append(key, value); } @@ -1574,26 +1913,28 @@ export class Avatars extends Service { } const apiPath = '/avatars/image'; - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof url !== 'undefined') { - payload['url'] = url; + apiPayload['url'] = url; } if (typeof width !== 'undefined') { - payload['width'] = width; + apiPayload['width'] = width; } if (typeof height !== 'undefined') { - payload['height'] = height; + apiPayload['height'] = height; } const uri = new URL(this.client.config.endpoint + apiPath); - payload['project'] = this.client.config.project; + apiPayload['project'] = this.client.config.project; - payload['impersonateuserid'] = this.client.config.impersonateuserid; + apiPayload['impersonateuserid'] = this.client.config.impersonateuserid; - for (const [key, value] of Object.entries(Service.flatten(payload))) { + for (const [key, value] of Object.entries( + Service.flatten(apiPayload), + )) { uri.searchParams.append(key, value); } @@ -1632,30 +1973,32 @@ export class Avatars extends Service { background?: string, ): URL { const apiPath = '/avatars/initials'; - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof name !== 'undefined') { - payload['name'] = name; + apiPayload['name'] = name; } if (typeof width !== 'undefined') { - payload['width'] = width; + apiPayload['width'] = width; } if (typeof height !== 'undefined') { - payload['height'] = height; + apiPayload['height'] = height; } if (typeof background !== 'undefined') { - payload['background'] = background; + apiPayload['background'] = background; } const uri = new URL(this.client.config.endpoint + apiPath); - payload['project'] = this.client.config.project; + apiPayload['project'] = this.client.config.project; - payload['impersonateuserid'] = this.client.config.impersonateuserid; + apiPayload['impersonateuserid'] = this.client.config.impersonateuserid; - for (const [key, value] of Object.entries(Service.flatten(payload))) { + for (const [key, value] of Object.entries( + Service.flatten(apiPayload), + )) { uri.searchParams.append(key, value); } @@ -1664,9 +2007,9 @@ export class Avatars extends Service { /** * Returns the best available profile photo for a user. The endpoint tries - * each source in priority order and returns the first successful result: - * OAuth2 identity photo, Gravatar, Libravatar, Appwrite Initials, built-in - * static fallback. + * each source in priority order and returns the first successful result: a + * custom uploaded photo (see avatars.updatePhoto), OAuth2 identity photo, + * Gravatar, Libravatar, Appwrite Initials, built-in static fallback. * * Passing `userId` — `current()` for the authenticated user — resolves * the photo from everything known about that user: identity photos, email, @@ -1701,46 +2044,48 @@ export class Avatars extends Service { name?: string, ): URL { const apiPath = '/avatars/photo'; - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof width !== 'undefined') { - payload['width'] = width; + apiPayload['width'] = width; } if (typeof height !== 'undefined') { - payload['height'] = height; + apiPayload['height'] = height; } if (typeof quality !== 'undefined') { - payload['quality'] = quality; + apiPayload['quality'] = quality; } if (typeof output !== 'undefined') { - payload['output'] = output; + apiPayload['output'] = output; } if (typeof rating !== 'undefined') { - payload['rating'] = rating; + apiPayload['rating'] = rating; } if (typeof userId !== 'undefined') { - payload['userId'] = userId; + apiPayload['userId'] = userId; } if (typeof emailHash !== 'undefined') { - payload['emailHash'] = emailHash; + apiPayload['emailHash'] = emailHash; } if (typeof name !== 'undefined') { - payload['name'] = name; + apiPayload['name'] = name; } const uri = new URL(this.client.config.endpoint + apiPath); - payload['project'] = this.client.config.project; + apiPayload['project'] = this.client.config.project; - payload['impersonateuserid'] = this.client.config.impersonateuserid; + apiPayload['impersonateuserid'] = this.client.config.impersonateuserid; - for (const [key, value] of Object.entries(Service.flatten(payload))) { + for (const [key, value] of Object.entries( + Service.flatten(apiPayload), + )) { uri.searchParams.append(key, value); } @@ -1770,30 +2115,32 @@ export class Avatars extends Service { } const apiPath = '/avatars/qr'; - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof text !== 'undefined') { - payload['text'] = text; + apiPayload['text'] = text; } if (typeof size !== 'undefined') { - payload['size'] = size; + apiPayload['size'] = size; } if (typeof margin !== 'undefined') { - payload['margin'] = margin; + apiPayload['margin'] = margin; } if (typeof download !== 'undefined') { - payload['download'] = download; + apiPayload['download'] = download; } const uri = new URL(this.client.config.endpoint + apiPath); - payload['project'] = this.client.config.project; + apiPayload['project'] = this.client.config.project; - payload['impersonateuserid'] = this.client.config.impersonateuserid; + apiPayload['impersonateuserid'] = this.client.config.impersonateuserid; - for (const [key, value] of Object.entries(Service.flatten(payload))) { + for (const [key, value] of Object.entries( + Service.flatten(apiPayload), + )) { uri.searchParams.append(key, value); } @@ -1862,94 +2209,96 @@ export class Avatars extends Service { } const apiPath = '/avatars/screenshots'; - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof url !== 'undefined') { - payload['url'] = url; + apiPayload['url'] = url; } if (typeof headers !== 'undefined') { - payload['headers'] = headers; + apiPayload['headers'] = headers; } if (typeof viewportWidth !== 'undefined') { - payload['viewportWidth'] = viewportWidth; + apiPayload['viewportWidth'] = viewportWidth; } if (typeof viewportHeight !== 'undefined') { - payload['viewportHeight'] = viewportHeight; + apiPayload['viewportHeight'] = viewportHeight; } if (typeof scale !== 'undefined') { - payload['scale'] = scale; + apiPayload['scale'] = scale; } if (typeof theme !== 'undefined') { - payload['theme'] = theme; + apiPayload['theme'] = theme; } if (typeof userAgent !== 'undefined') { - payload['userAgent'] = userAgent; + apiPayload['userAgent'] = userAgent; } if (typeof fullpage !== 'undefined') { - payload['fullpage'] = fullpage; + apiPayload['fullpage'] = fullpage; } if (typeof locale !== 'undefined') { - payload['locale'] = locale; + apiPayload['locale'] = locale; } if (typeof timezone !== 'undefined') { - payload['timezone'] = timezone; + apiPayload['timezone'] = timezone; } if (typeof latitude !== 'undefined') { - payload['latitude'] = latitude; + apiPayload['latitude'] = latitude; } if (typeof longitude !== 'undefined') { - payload['longitude'] = longitude; + apiPayload['longitude'] = longitude; } if (typeof accuracy !== 'undefined') { - payload['accuracy'] = accuracy; + apiPayload['accuracy'] = accuracy; } if (typeof touch !== 'undefined') { - payload['touch'] = touch; + apiPayload['touch'] = touch; } if (typeof permissions !== 'undefined') { - payload['permissions'] = permissions; + apiPayload['permissions'] = permissions; } if (typeof sleep !== 'undefined') { - payload['sleep'] = sleep; + apiPayload['sleep'] = sleep; } if (typeof width !== 'undefined') { - payload['width'] = width; + apiPayload['width'] = width; } if (typeof height !== 'undefined') { - payload['height'] = height; + apiPayload['height'] = height; } if (typeof quality !== 'undefined') { - payload['quality'] = quality; + apiPayload['quality'] = quality; } if (typeof output !== 'undefined') { - payload['output'] = output; + apiPayload['output'] = output; } const uri = new URL(this.client.config.endpoint + apiPath); - payload['project'] = this.client.config.project; + apiPayload['project'] = this.client.config.project; - payload['impersonateuserid'] = this.client.config.impersonateuserid; + apiPayload['impersonateuserid'] = this.client.config.impersonateuserid; - for (const [key, value] of Object.entries(Service.flatten(payload))) { + for (const [key, value] of Object.entries( + Service.flatten(apiPayload), + )) { uri.searchParams.append(key, value); } diff --git a/src/services/databases.ts b/src/services/databases.ts index 8bf59521..94d2ad16 100644 --- a/src/services/databases.ts +++ b/src/services/databases.ts @@ -12,47 +12,63 @@ export class Databases extends Service { * List transactions across all databases. * * @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). + * @param {boolean} params.total - When set to false, the total count returned will be 0 and will not be calculated. * @throws {AppwriteException} * @returns {Promise} * @deprecated This API has been deprecated since 1.8.0. Please use `TablesDB.listTransactions` instead. */ listTransactions(params?: { queries?: string[]; + total?: boolean; }): Promise; /** * List transactions across all databases. * * @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). + * @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. */ - listTransactions(queries?: string[]): Promise; listTransactions( - paramsOrFirst?: { queries?: string[] } | string[], + queries?: string[], + total?: boolean, + ): Promise; + listTransactions( + paramsOrFirst?: { queries?: string[]; total?: boolean } | string[], + ...rest: [boolean?] ): Promise { - let params: { queries?: string[] }; + let params: { queries?: string[]; total?: boolean }; if ( - typeof paramsOrFirst === 'undefined' || + (typeof paramsOrFirst === 'undefined' && rest.length === 0) || (paramsOrFirst && typeof paramsOrFirst === 'object' && !Array.isArray(paramsOrFirst)) ) { - params = (paramsOrFirst || {}) as { queries?: string[] }; + 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 = '/databases/transactions'; - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof queries !== 'undefined') { - payload['queries'] = queries; + apiPayload['queries'] = queries; + } + + if (typeof total !== 'undefined') { + apiPayload['total'] = total; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -63,7 +79,7 @@ export class Databases extends Service { 'X-Appwrite-Project': this.client.config.project, accept: 'application/json', }, - payload, + apiPayload, ); } @@ -106,10 +122,10 @@ export class Databases extends Service { const ttl = params.ttl; const apiPath = '/databases/transactions'; - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof ttl !== 'undefined') { - payload['ttl'] = ttl; + apiPayload['ttl'] = ttl; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -121,7 +137,7 @@ export class Databases extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -174,7 +190,7 @@ export class Databases extends Service { '{transactionId}', encodeURIComponent(String(transactionId)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; const uri = new URL(this.client.config.endpoint + apiPath); return this.client.call( @@ -184,7 +200,7 @@ export class Databases extends Service { 'X-Appwrite-Project': this.client.config.project, accept: 'application/json', }, - payload, + apiPayload, ); } @@ -262,14 +278,14 @@ export class Databases extends Service { '{transactionId}', encodeURIComponent(String(transactionId)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof commit !== 'undefined') { - payload['commit'] = commit; + apiPayload['commit'] = commit; } if (typeof rollback !== 'undefined') { - payload['rollback'] = rollback; + apiPayload['rollback'] = rollback; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -281,7 +297,7 @@ export class Databases extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -332,7 +348,7 @@ export class Databases extends Service { '{transactionId}', encodeURIComponent(String(transactionId)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; const uri = new URL(this.client.config.endpoint + apiPath); return this.client.call( @@ -343,7 +359,7 @@ export class Databases extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -410,10 +426,10 @@ export class Databases extends Service { '{transactionId}', encodeURIComponent(String(transactionId)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof operations !== 'undefined') { - payload['operations'] = operations; + apiPayload['operations'] = operations; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -425,7 +441,7 @@ export class Databases extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -545,22 +561,22 @@ export class Databases extends Service { '{collectionId}', encodeURIComponent(String(collectionId)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof queries !== 'undefined') { - payload['queries'] = queries; + apiPayload['queries'] = queries; } if (typeof transactionId !== 'undefined') { - payload['transactionId'] = transactionId; + apiPayload['transactionId'] = transactionId; } if (typeof total !== 'undefined') { - payload['total'] = total; + apiPayload['total'] = total; } if (typeof ttl !== 'undefined') { - payload['ttl'] = ttl; + apiPayload['ttl'] = ttl; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -571,7 +587,7 @@ export class Databases extends Service { 'X-Appwrite-Project': this.client.config.project, accept: 'application/json', }, - payload, + apiPayload, ); } @@ -726,22 +742,22 @@ export class Databases extends Service { '{collectionId}', encodeURIComponent(String(collectionId)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof documentId !== 'undefined') { - payload['documentId'] = documentId; + apiPayload['documentId'] = documentId; } if (typeof data !== 'undefined') { - payload['data'] = data; + apiPayload['data'] = data; } if (typeof permissions !== 'undefined') { - payload['permissions'] = permissions; + apiPayload['permissions'] = permissions; } if (typeof transactionId !== 'undefined') { - payload['transactionId'] = transactionId; + apiPayload['transactionId'] = transactionId; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -753,7 +769,7 @@ export class Databases extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -874,14 +890,14 @@ export class Databases extends Service { '{documentId}', encodeURIComponent(String(documentId)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof queries !== 'undefined') { - payload['queries'] = queries; + apiPayload['queries'] = queries; } if (typeof transactionId !== 'undefined') { - payload['transactionId'] = transactionId; + apiPayload['transactionId'] = transactionId; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -892,7 +908,7 @@ export class Databases extends Service { 'X-Appwrite-Project': this.client.config.project, accept: 'application/json', }, - payload, + apiPayload, ); } @@ -1049,18 +1065,18 @@ export class Databases extends Service { '{documentId}', encodeURIComponent(String(documentId)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof data !== 'undefined') { - payload['data'] = data; + apiPayload['data'] = data; } if (typeof permissions !== 'undefined') { - payload['permissions'] = permissions; + apiPayload['permissions'] = permissions; } if (typeof transactionId !== 'undefined') { - payload['transactionId'] = transactionId; + apiPayload['transactionId'] = transactionId; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -1072,7 +1088,7 @@ export class Databases extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -1229,18 +1245,18 @@ export class Databases extends Service { '{documentId}', encodeURIComponent(String(documentId)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof data !== 'undefined') { - payload['data'] = data; + apiPayload['data'] = data; } if (typeof permissions !== 'undefined') { - payload['permissions'] = permissions; + apiPayload['permissions'] = permissions; } if (typeof transactionId !== 'undefined') { - payload['transactionId'] = transactionId; + apiPayload['transactionId'] = transactionId; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -1252,7 +1268,7 @@ export class Databases extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -1362,10 +1378,10 @@ export class Databases extends Service { '{documentId}', encodeURIComponent(String(documentId)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof transactionId !== 'undefined') { - payload['transactionId'] = transactionId; + apiPayload['transactionId'] = transactionId; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -1377,7 +1393,7 @@ export class Databases extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -1524,18 +1540,18 @@ export class Databases extends Service { ) .replace('{documentId}', encodeURIComponent(String(documentId))) .replace('{attribute}', encodeURIComponent(String(attribute))); - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof value !== 'undefined') { - payload['value'] = value; + apiPayload['value'] = value; } if (typeof min !== 'undefined') { - payload['min'] = min; + apiPayload['min'] = min; } if (typeof transactionId !== 'undefined') { - payload['transactionId'] = transactionId; + apiPayload['transactionId'] = transactionId; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -1547,7 +1563,7 @@ export class Databases extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -1694,18 +1710,18 @@ export class Databases extends Service { ) .replace('{documentId}', encodeURIComponent(String(documentId))) .replace('{attribute}', encodeURIComponent(String(attribute))); - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof value !== 'undefined') { - payload['value'] = value; + apiPayload['value'] = value; } if (typeof max !== 'undefined') { - payload['max'] = max; + apiPayload['max'] = max; } if (typeof transactionId !== 'undefined') { - payload['transactionId'] = transactionId; + apiPayload['transactionId'] = transactionId; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -1717,7 +1733,7 @@ export class Databases extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } } diff --git a/src/services/documents-db.ts b/src/services/documents-db.ts index 082627d1..6f10966e 100644 --- a/src/services/documents-db.ts +++ b/src/services/documents-db.ts @@ -12,46 +12,62 @@ export class DocumentsDB extends Service { * List transactions across all databases. * * @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). + * @param {boolean} params.total - When set to false, the total count returned will be 0 and will not be calculated. * @throws {AppwriteException} * @returns {Promise} */ listTransactions(params?: { queries?: string[]; + total?: boolean; }): Promise; /** * List transactions across all databases. * * @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). + * @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. */ - listTransactions(queries?: string[]): Promise; listTransactions( - paramsOrFirst?: { queries?: string[] } | string[], + queries?: string[], + total?: boolean, + ): Promise; + listTransactions( + paramsOrFirst?: { queries?: string[]; total?: boolean } | string[], + ...rest: [boolean?] ): Promise { - let params: { queries?: string[] }; + let params: { queries?: string[]; total?: boolean }; if ( - typeof paramsOrFirst === 'undefined' || + (typeof paramsOrFirst === 'undefined' && rest.length === 0) || (paramsOrFirst && typeof paramsOrFirst === 'object' && !Array.isArray(paramsOrFirst)) ) { - params = (paramsOrFirst || {}) as { queries?: string[] }; + 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 = '/documentsdb/transactions'; - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof queries !== 'undefined') { - payload['queries'] = queries; + apiPayload['queries'] = queries; + } + + if (typeof total !== 'undefined') { + apiPayload['total'] = total; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -62,7 +78,7 @@ export class DocumentsDB extends Service { 'X-Appwrite-Project': this.client.config.project, accept: 'application/json', }, - payload, + apiPayload, ); } @@ -104,10 +120,10 @@ export class DocumentsDB extends Service { const ttl = params.ttl; const apiPath = '/documentsdb/transactions'; - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof ttl !== 'undefined') { - payload['ttl'] = ttl; + apiPayload['ttl'] = ttl; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -119,7 +135,7 @@ export class DocumentsDB extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -171,7 +187,7 @@ export class DocumentsDB extends Service { '{transactionId}', encodeURIComponent(String(transactionId)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; const uri = new URL(this.client.config.endpoint + apiPath); return this.client.call( @@ -181,7 +197,7 @@ export class DocumentsDB extends Service { 'X-Appwrite-Project': this.client.config.project, accept: 'application/json', }, - payload, + apiPayload, ); } @@ -258,14 +274,14 @@ export class DocumentsDB extends Service { '{transactionId}', encodeURIComponent(String(transactionId)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof commit !== 'undefined') { - payload['commit'] = commit; + apiPayload['commit'] = commit; } if (typeof rollback !== 'undefined') { - payload['rollback'] = rollback; + apiPayload['rollback'] = rollback; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -277,7 +293,7 @@ export class DocumentsDB extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -327,7 +343,7 @@ export class DocumentsDB extends Service { '{transactionId}', encodeURIComponent(String(transactionId)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; const uri = new URL(this.client.config.endpoint + apiPath); return this.client.call( @@ -338,7 +354,7 @@ export class DocumentsDB extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -404,10 +420,10 @@ export class DocumentsDB extends Service { '{transactionId}', encodeURIComponent(String(transactionId)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof operations !== 'undefined') { - payload['operations'] = operations; + apiPayload['operations'] = operations; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -419,7 +435,7 @@ export class DocumentsDB extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -538,22 +554,22 @@ export class DocumentsDB extends Service { '{collectionId}', encodeURIComponent(String(collectionId)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof queries !== 'undefined') { - payload['queries'] = queries; + apiPayload['queries'] = queries; } if (typeof transactionId !== 'undefined') { - payload['transactionId'] = transactionId; + apiPayload['transactionId'] = transactionId; } if (typeof total !== 'undefined') { - payload['total'] = total; + apiPayload['total'] = total; } if (typeof ttl !== 'undefined') { - payload['ttl'] = ttl; + apiPayload['ttl'] = ttl; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -564,7 +580,7 @@ export class DocumentsDB extends Service { 'X-Appwrite-Project': this.client.config.project, accept: 'application/json', }, - payload, + apiPayload, ); } @@ -718,22 +734,22 @@ export class DocumentsDB extends Service { '{collectionId}', encodeURIComponent(String(collectionId)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof documentId !== 'undefined') { - payload['documentId'] = documentId; + apiPayload['documentId'] = documentId; } if (typeof data !== 'undefined') { - payload['data'] = data; + apiPayload['data'] = data; } if (typeof permissions !== 'undefined') { - payload['permissions'] = permissions; + apiPayload['permissions'] = permissions; } if (typeof transactionId !== 'undefined') { - payload['transactionId'] = transactionId; + apiPayload['transactionId'] = transactionId; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -745,7 +761,7 @@ export class DocumentsDB extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -852,14 +868,14 @@ export class DocumentsDB extends Service { '{collectionId}', encodeURIComponent(String(collectionId)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof documents !== 'undefined') { - payload['documents'] = documents; + apiPayload['documents'] = documents; } if (typeof transactionId !== 'undefined') { - payload['transactionId'] = transactionId; + apiPayload['transactionId'] = transactionId; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -871,7 +887,7 @@ export class DocumentsDB extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -991,14 +1007,14 @@ export class DocumentsDB extends Service { '{documentId}', encodeURIComponent(String(documentId)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof queries !== 'undefined') { - payload['queries'] = queries; + apiPayload['queries'] = queries; } if (typeof transactionId !== 'undefined') { - payload['transactionId'] = transactionId; + apiPayload['transactionId'] = transactionId; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -1009,7 +1025,7 @@ export class DocumentsDB extends Service { 'X-Appwrite-Project': this.client.config.project, accept: 'application/json', }, - payload, + apiPayload, ); } @@ -1165,18 +1181,18 @@ export class DocumentsDB extends Service { '{documentId}', encodeURIComponent(String(documentId)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof data !== 'undefined') { - payload['data'] = data; + apiPayload['data'] = data; } if (typeof permissions !== 'undefined') { - payload['permissions'] = permissions; + apiPayload['permissions'] = permissions; } if (typeof transactionId !== 'undefined') { - payload['transactionId'] = transactionId; + apiPayload['transactionId'] = transactionId; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -1188,7 +1204,7 @@ export class DocumentsDB extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -1344,18 +1360,18 @@ export class DocumentsDB extends Service { '{documentId}', encodeURIComponent(String(documentId)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof data !== 'undefined') { - payload['data'] = data; + apiPayload['data'] = data; } if (typeof permissions !== 'undefined') { - payload['permissions'] = permissions; + apiPayload['permissions'] = permissions; } if (typeof transactionId !== 'undefined') { - payload['transactionId'] = transactionId; + apiPayload['transactionId'] = transactionId; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -1367,7 +1383,7 @@ export class DocumentsDB extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -1476,10 +1492,10 @@ export class DocumentsDB extends Service { '{documentId}', encodeURIComponent(String(documentId)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof transactionId !== 'undefined') { - payload['transactionId'] = transactionId; + apiPayload['transactionId'] = transactionId; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -1491,7 +1507,7 @@ export class DocumentsDB extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -1637,18 +1653,18 @@ export class DocumentsDB extends Service { ) .replace('{documentId}', encodeURIComponent(String(documentId))) .replace('{attribute}', encodeURIComponent(String(attribute))); - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof value !== 'undefined') { - payload['value'] = value; + apiPayload['value'] = value; } if (typeof min !== 'undefined') { - payload['min'] = min; + apiPayload['min'] = min; } if (typeof transactionId !== 'undefined') { - payload['transactionId'] = transactionId; + apiPayload['transactionId'] = transactionId; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -1660,7 +1676,7 @@ export class DocumentsDB extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -1806,18 +1822,18 @@ export class DocumentsDB extends Service { ) .replace('{documentId}', encodeURIComponent(String(documentId))) .replace('{attribute}', encodeURIComponent(String(attribute))); - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof value !== 'undefined') { - payload['value'] = value; + apiPayload['value'] = value; } if (typeof max !== 'undefined') { - payload['max'] = max; + apiPayload['max'] = max; } if (typeof transactionId !== 'undefined') { - payload['transactionId'] = transactionId; + apiPayload['transactionId'] = transactionId; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -1829,7 +1845,7 @@ export class DocumentsDB extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } } diff --git a/src/services/functions.ts b/src/services/functions.ts index 3366f2db..230ffe70 100644 --- a/src/services/functions.ts +++ b/src/services/functions.ts @@ -78,14 +78,14 @@ export class Functions extends Service { '{functionId}', encodeURIComponent(String(functionId)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof queries !== 'undefined') { - payload['queries'] = queries; + apiPayload['queries'] = queries; } if (typeof total !== 'undefined') { - payload['total'] = total; + apiPayload['total'] = total; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -96,7 +96,7 @@ export class Functions extends Service { 'X-Appwrite-Project': this.client.config.project, accept: 'application/json', }, - payload, + apiPayload, ); } @@ -220,30 +220,30 @@ export class Functions extends Service { '{functionId}', encodeURIComponent(String(functionId)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof body !== 'undefined') { - payload['body'] = body; + apiPayload['body'] = body; } if (typeof async !== 'undefined') { - payload['async'] = async; + apiPayload['async'] = async; } if (typeof xpath !== 'undefined') { - payload['path'] = xpath; + apiPayload['path'] = xpath; } if (typeof method !== 'undefined') { - payload['method'] = method; + apiPayload['method'] = method; } if (typeof headers !== 'undefined') { - payload['headers'] = headers; + apiPayload['headers'] = headers; } if (typeof scheduledAt !== 'undefined') { - payload['scheduledAt'] = scheduledAt; + apiPayload['scheduledAt'] = scheduledAt; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -255,7 +255,7 @@ export class Functions extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -324,7 +324,7 @@ export class Functions extends Service { const apiPath = '/functions/{functionId}/executions/{executionId}' .replace('{functionId}', encodeURIComponent(String(functionId))) .replace('{executionId}', encodeURIComponent(String(executionId))); - const payload: Payload = {}; + const apiPayload: Payload = {}; const uri = new URL(this.client.config.endpoint + apiPath); return this.client.call( @@ -334,7 +334,7 @@ export class Functions extends Service { 'X-Appwrite-Project': this.client.config.project, accept: 'application/json', }, - payload, + apiPayload, ); } } diff --git a/src/services/graphql.ts b/src/services/graphql.ts index b08fe7ce..f8b21a91 100644 --- a/src/services/graphql.ts +++ b/src/services/graphql.ts @@ -47,10 +47,10 @@ export class Graphql extends Service { } const apiPath = '/graphql'; - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof query !== 'undefined') { - payload['query'] = query; + apiPayload['query'] = query; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -63,7 +63,7 @@ export class Graphql extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -107,10 +107,10 @@ export class Graphql extends Service { } const apiPath = '/graphql/mutation'; - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof query !== 'undefined') { - payload['query'] = query; + apiPayload['query'] = query; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -123,7 +123,7 @@ export class Graphql extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } } diff --git a/src/services/locale.ts b/src/services/locale.ts index 6259730a..ce9e92b2 100644 --- a/src/services/locale.ts +++ b/src/services/locale.ts @@ -18,7 +18,7 @@ export class Locale extends Service { */ get(): Promise { const apiPath = '/locale'; - const payload: Payload = {}; + const apiPayload: Payload = {}; const uri = new URL(this.client.config.endpoint + apiPath); return this.client.call( @@ -28,19 +28,53 @@ export class Locale extends Service { 'X-Appwrite-Project': this.client.config.project, accept: 'application/json', }, - payload, + apiPayload, ); } /** * List of all locale codes in [ISO 639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes). * + * @param {boolean} params.total - When set to false, the total count returned will be 0 and will not be calculated. * @throws {AppwriteException} * @returns {Promise} */ - listCodes(): Promise { + listCodes(params?: { total?: boolean }): Promise; + /** + * List of all locale codes in [ISO 639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes). + * + * @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. + */ + listCodes(total?: boolean): Promise; + listCodes( + paramsOrFirst?: { total?: boolean } | boolean, + ): Promise { + let params: { total?: boolean }; + + if ( + typeof paramsOrFirst === 'undefined' || + (paramsOrFirst && + typeof paramsOrFirst === 'object' && + !Array.isArray(paramsOrFirst)) + ) { + params = (paramsOrFirst || {}) as { total?: boolean }; + } else { + params = { + total: paramsOrFirst as boolean, + }; + } + + const total = params.total; + const apiPath = '/locale/codes'; - const payload: Payload = {}; + const apiPayload: Payload = {}; + + if (typeof total !== 'undefined') { + apiPayload['total'] = total; + } const uri = new URL(this.client.config.endpoint + apiPath); return this.client.call( @@ -50,19 +84,53 @@ export class Locale extends Service { 'X-Appwrite-Project': this.client.config.project, accept: 'application/json', }, - payload, + apiPayload, ); } /** * List of all continents. You can use the locale header to get the data in a supported language. * + * @param {boolean} params.total - When set to false, the total count returned will be 0 and will not be calculated. * @throws {AppwriteException} * @returns {Promise} */ - listContinents(): Promise { + listContinents(params?: { total?: boolean }): Promise; + /** + * List of all continents. You can use the locale header to get the data in a supported language. + * + * @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. + */ + listContinents(total?: boolean): Promise; + listContinents( + paramsOrFirst?: { total?: boolean } | boolean, + ): Promise { + let params: { total?: boolean }; + + if ( + typeof paramsOrFirst === 'undefined' || + (paramsOrFirst && + typeof paramsOrFirst === 'object' && + !Array.isArray(paramsOrFirst)) + ) { + params = (paramsOrFirst || {}) as { total?: boolean }; + } else { + params = { + total: paramsOrFirst as boolean, + }; + } + + const total = params.total; + const apiPath = '/locale/continents'; - const payload: Payload = {}; + const apiPayload: Payload = {}; + + if (typeof total !== 'undefined') { + apiPayload['total'] = total; + } const uri = new URL(this.client.config.endpoint + apiPath); return this.client.call( @@ -72,19 +140,53 @@ export class Locale extends Service { 'X-Appwrite-Project': this.client.config.project, accept: 'application/json', }, - payload, + apiPayload, ); } /** * List of all countries. You can use the locale header to get the data in a supported language. * + * @param {boolean} params.total - When set to false, the total count returned will be 0 and will not be calculated. * @throws {AppwriteException} * @returns {Promise} */ - listCountries(): Promise { + listCountries(params?: { total?: boolean }): Promise; + /** + * List of all countries. You can use the locale header to get the data in a supported language. + * + * @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. + */ + listCountries(total?: boolean): Promise; + listCountries( + paramsOrFirst?: { total?: boolean } | boolean, + ): Promise { + let params: { total?: boolean }; + + if ( + typeof paramsOrFirst === 'undefined' || + (paramsOrFirst && + typeof paramsOrFirst === 'object' && + !Array.isArray(paramsOrFirst)) + ) { + params = (paramsOrFirst || {}) as { total?: boolean }; + } else { + params = { + total: paramsOrFirst as boolean, + }; + } + + const total = params.total; + const apiPath = '/locale/countries'; - const payload: Payload = {}; + const apiPayload: Payload = {}; + + if (typeof total !== 'undefined') { + apiPayload['total'] = total; + } const uri = new URL(this.client.config.endpoint + apiPath); return this.client.call( @@ -94,19 +196,53 @@ export class Locale extends Service { 'X-Appwrite-Project': this.client.config.project, accept: 'application/json', }, - payload, + apiPayload, ); } /** * List of all countries that are currently members of the EU. You can use the locale header to get the data in a supported language. * + * @param {boolean} params.total - When set to false, the total count returned will be 0 and will not be calculated. * @throws {AppwriteException} * @returns {Promise} */ - listCountriesEU(): Promise { + listCountriesEU(params?: { total?: boolean }): Promise; + /** + * List of all countries that are currently members of the EU. You can use the locale header to get the data in a supported language. + * + * @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. + */ + listCountriesEU(total?: boolean): Promise; + listCountriesEU( + paramsOrFirst?: { total?: boolean } | boolean, + ): Promise { + let params: { total?: boolean }; + + if ( + typeof paramsOrFirst === 'undefined' || + (paramsOrFirst && + typeof paramsOrFirst === 'object' && + !Array.isArray(paramsOrFirst)) + ) { + params = (paramsOrFirst || {}) as { total?: boolean }; + } else { + params = { + total: paramsOrFirst as boolean, + }; + } + + const total = params.total; + const apiPath = '/locale/countries/eu'; - const payload: Payload = {}; + const apiPayload: Payload = {}; + + if (typeof total !== 'undefined') { + apiPayload['total'] = total; + } const uri = new URL(this.client.config.endpoint + apiPath); return this.client.call( @@ -116,19 +252,55 @@ export class Locale extends Service { 'X-Appwrite-Project': this.client.config.project, accept: 'application/json', }, - payload, + apiPayload, ); } /** * List of all countries phone codes. You can use the locale header to get the data in a supported language. * + * @param {boolean} params.total - When set to false, the total count returned will be 0 and will not be calculated. * @throws {AppwriteException} * @returns {Promise} */ - listCountriesPhones(): Promise { + listCountriesPhones(params?: { + total?: boolean; + }): Promise; + /** + * List of all countries phone codes. You can use the locale header to get the data in a supported language. + * + * @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. + */ + listCountriesPhones(total?: boolean): Promise; + listCountriesPhones( + paramsOrFirst?: { total?: boolean } | boolean, + ): Promise { + let params: { total?: boolean }; + + if ( + typeof paramsOrFirst === 'undefined' || + (paramsOrFirst && + typeof paramsOrFirst === 'object' && + !Array.isArray(paramsOrFirst)) + ) { + params = (paramsOrFirst || {}) as { total?: boolean }; + } else { + params = { + total: paramsOrFirst as boolean, + }; + } + + const total = params.total; + const apiPath = '/locale/countries/phones'; - const payload: Payload = {}; + const apiPayload: Payload = {}; + + if (typeof total !== 'undefined') { + apiPayload['total'] = total; + } const uri = new URL(this.client.config.endpoint + apiPath); return this.client.call( @@ -138,19 +310,53 @@ export class Locale extends Service { 'X-Appwrite-Project': this.client.config.project, accept: 'application/json', }, - payload, + apiPayload, ); } /** * List of all currencies, including currency symbol, name, plural, and decimal digits for all major and minor currencies. You can use the locale header to get the data in a supported language. * + * @param {boolean} params.total - When set to false, the total count returned will be 0 and will not be calculated. * @throws {AppwriteException} * @returns {Promise} */ - listCurrencies(): Promise { + listCurrencies(params?: { total?: boolean }): Promise; + /** + * List of all currencies, including currency symbol, name, plural, and decimal digits for all major and minor currencies. You can use the locale header to get the data in a supported language. + * + * @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. + */ + listCurrencies(total?: boolean): Promise; + listCurrencies( + paramsOrFirst?: { total?: boolean } | boolean, + ): Promise { + let params: { total?: boolean }; + + if ( + typeof paramsOrFirst === 'undefined' || + (paramsOrFirst && + typeof paramsOrFirst === 'object' && + !Array.isArray(paramsOrFirst)) + ) { + params = (paramsOrFirst || {}) as { total?: boolean }; + } else { + params = { + total: paramsOrFirst as boolean, + }; + } + + const total = params.total; + const apiPath = '/locale/currencies'; - const payload: Payload = {}; + const apiPayload: Payload = {}; + + if (typeof total !== 'undefined') { + apiPayload['total'] = total; + } const uri = new URL(this.client.config.endpoint + apiPath); return this.client.call( @@ -160,19 +366,53 @@ export class Locale extends Service { 'X-Appwrite-Project': this.client.config.project, accept: 'application/json', }, - payload, + apiPayload, ); } /** * List of all languages classified by ISO 639-1 including 2-letter code, name in English, and name in the respective language. * + * @param {boolean} params.total - When set to false, the total count returned will be 0 and will not be calculated. * @throws {AppwriteException} * @returns {Promise} */ - listLanguages(): Promise { + listLanguages(params?: { total?: boolean }): Promise; + /** + * List of all languages classified by ISO 639-1 including 2-letter code, name in English, and name in the respective language. + * + * @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. + */ + listLanguages(total?: boolean): Promise; + listLanguages( + paramsOrFirst?: { total?: boolean } | boolean, + ): Promise { + let params: { total?: boolean }; + + if ( + typeof paramsOrFirst === 'undefined' || + (paramsOrFirst && + typeof paramsOrFirst === 'object' && + !Array.isArray(paramsOrFirst)) + ) { + params = (paramsOrFirst || {}) as { total?: boolean }; + } else { + params = { + total: paramsOrFirst as boolean, + }; + } + + const total = params.total; + const apiPath = '/locale/languages'; - const payload: Payload = {}; + const apiPayload: Payload = {}; + + if (typeof total !== 'undefined') { + apiPayload['total'] = total; + } const uri = new URL(this.client.config.endpoint + apiPath); return this.client.call( @@ -182,7 +422,7 @@ export class Locale extends Service { 'X-Appwrite-Project': this.client.config.project, accept: 'application/json', }, - payload, + apiPayload, ); } } diff --git a/src/services/messaging.ts b/src/services/messaging.ts index 88f88a29..33e94c19 100644 --- a/src/services/messaging.ts +++ b/src/services/messaging.ts @@ -89,14 +89,14 @@ export class Messaging extends Service { '{topicId}', encodeURIComponent(String(topicId)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof subscriberId !== 'undefined') { - payload['subscriberId'] = subscriberId; + apiPayload['subscriberId'] = subscriberId; } if (typeof targetId !== 'undefined') { - payload['targetId'] = targetId; + apiPayload['targetId'] = targetId; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -108,7 +108,7 @@ export class Messaging extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -177,7 +177,7 @@ export class Messaging extends Service { '{subscriberId}', encodeURIComponent(String(subscriberId)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; const uri = new URL(this.client.config.endpoint + apiPath); return this.client.call( @@ -188,7 +188,7 @@ export class Messaging extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } } diff --git a/src/services/oauth-2.ts b/src/services/oauth-2.ts new file mode 100644 index 00000000..1920bc1a --- /dev/null +++ b/src/services/oauth-2.ts @@ -0,0 +1,1956 @@ +import { Service } from '../service'; +import { AppwriteException, Client } from '../client'; +import type { Models } from '../models'; +import type { Payload } from '../client'; + +export class Oauth2 extends Service { + constructor(client: Client) { + super(client); + } + + /** + * Approve an OAuth2 grant after the user gives consent. Returns the `redirectUrl` the end user should be sent to. The consent screen may optionally pass enriched `authorization_details` to record the concrete resources the user selected. You can pass Accept header of `application/json` to receive a JSON response instead of a redirect. + * + * @param {string} params.grantId - Grant ID made during authorization, provided to consent screen in URL search params. + * @param {string} params.authorizationDetails - Enriched `authorization_details` the user consented to, replacing what the client requested. Each entry must use a `type` the project accepts. Optional; omit to keep the originally requested details. + * @param {string} params.scope - Space-separated scopes the user consented to. Must be a subset of the scopes originally requested; identity scopes such as `openid` are always retained. Optional; omit to keep the originally requested scopes. + * @throws {AppwriteException} + * @returns {Promise} + */ + approve(params: { + grantId: string; + authorizationDetails?: string; + scope?: string; + }): Promise; + /** + * Approve an OAuth2 grant after the user gives consent. Returns the `redirectUrl` the end user should be sent to. The consent screen may optionally pass enriched `authorization_details` to record the concrete resources the user selected. You can pass Accept header of `application/json` to receive a JSON response instead of a redirect. + * + * @param {string} grantId - Grant ID made during authorization, provided to consent screen in URL search params. + * @param {string} authorizationDetails - Enriched `authorization_details` the user consented to, replacing what the client requested. Each entry must use a `type` the project accepts. Optional; omit to keep the originally requested details. + * @param {string} scope - Space-separated scopes the user consented to. Must be a subset of the scopes originally requested; identity scopes such as `openid` are always retained. Optional; omit to keep the originally requested scopes. + * @throws {AppwriteException} + * @returns {Promise} + * @deprecated Use the object parameter style method for a better developer experience. + */ + approve( + grantId: string, + authorizationDetails?: string, + scope?: string, + ): Promise; + approve( + paramsOrFirst: + | { grantId: string; authorizationDetails?: string; scope?: string } + | string, + ...rest: [string?, string?] + ): Promise { + let params: { + grantId: string; + authorizationDetails?: string; + scope?: string; + }; + + if ( + paramsOrFirst && + typeof paramsOrFirst === 'object' && + !Array.isArray(paramsOrFirst) + ) { + params = (paramsOrFirst || {}) as { + grantId: string; + authorizationDetails?: string; + scope?: string; + }; + } else { + params = { + grantId: paramsOrFirst as string, + authorizationDetails: rest[0] as string, + scope: rest[1] as string, + }; + } + + const grantId = params.grantId; + const authorizationDetails = params.authorizationDetails; + const scope = params.scope; + + if (typeof grantId === 'undefined') { + throw new AppwriteException( + 'Missing required parameter: "grantId"', + ); + } + + const apiPath = '/oauth2/{project_id}/approve'.replace( + '{project_id}', + encodeURIComponent(String(this.client.config.project)), + ); + const apiPayload: Payload = {}; + + if (typeof grantId !== 'undefined') { + apiPayload['grant_id'] = grantId; + } + + if (typeof authorizationDetails !== 'undefined') { + apiPayload['authorization_details'] = authorizationDetails; + } + + if (typeof scope !== 'undefined') { + apiPayload['scope'] = scope; + } + + const uri = new URL(this.client.config.endpoint + apiPath); + return this.client.call( + 'post', + uri, + { + 'content-type': 'application/json', + accept: 'application/json', + }, + apiPayload, + ); + } + + /** + * Begin the OAuth2 authorization flow. When called without a session, the user is redirected to the consent screen without grant ID. When called with a session, the redirect URL includes param for grant ID. You can pass Accept header of `application/json` to receive a JSON response instead of a redirect. + * + * @param {string} params.clientId - OAuth2 client ID. Either a registered app ID or an HTTPS client ID metadata document URL. + * @param {string} params.redirectUri - Redirect URI where visitor will be redirected after authorization, whether successful or not. + * @param {string} params.responseType - OAuth2 / OIDC response type. One of `code` (Authorization Code Flow), `id_token` (Implicit Flow, OIDC login only), or `code id_token` (Hybrid Flow). + * @param {string} params.scope - Space-separated OAuth2 scopes. Can include project scopes, and built-in scopes: `openid`, `email`, `profile`, `phone`. + * @param {string} params.state - OAuth2 state. You receive this back in the redirect URI. + * @param {string} params.nonce - OIDC nonce parameter to prevent replay attacks. Required when response_type includes `id_token`. + * @param {string} params.codeChallenge - PKCE code challenge. Required when OAuth2 app is public. + * @param {string} params.codeChallengeMethod - PKCE code challenge method. Required when OAuth2 app is public. + * @param {string} params.prompt - OIDC prompt parameter for customization of consent screen. Space-separated list of: none, login, consent, select_account. + * @param {number} params.maxAge - OIDC max_age paraleter for customization of consent screen. Maximum allowable elapsed time in seconds since the user last authenticated. If exceeded, re-authentication is required. + * @param {string} params.authorizationDetails - Rich authorization request. JSON array of objects, each with a `type` and project-defined fields + * @param {string} params.resource - RFC 8707 resource indicator URI or URI list. Each value must be an absolute URI without a fragment. + * @param {string} params.audience - Compatibility alias for a single OAuth2 resource indicator URI. + * @param {string} params.requestUri - OAuth2 authorization request handle returned by the pushed authorization request endpoint. + * @throws {AppwriteException} + * @returns {Promise} + */ + authorize(params?: { + clientId?: string; + redirectUri?: string; + responseType?: string; + scope?: string; + state?: string; + nonce?: string; + codeChallenge?: string; + codeChallengeMethod?: string; + prompt?: string; + maxAge?: number; + authorizationDetails?: string; + resource?: string; + audience?: string; + requestUri?: string; + }): Promise; + /** + * Begin the OAuth2 authorization flow. When called without a session, the user is redirected to the consent screen without grant ID. When called with a session, the redirect URL includes param for grant ID. You can pass Accept header of `application/json` to receive a JSON response instead of a redirect. + * + * @param {string} clientId - OAuth2 client ID. Either a registered app ID or an HTTPS client ID metadata document URL. + * @param {string} redirectUri - Redirect URI where visitor will be redirected after authorization, whether successful or not. + * @param {string} responseType - OAuth2 / OIDC response type. One of `code` (Authorization Code Flow), `id_token` (Implicit Flow, OIDC login only), or `code id_token` (Hybrid Flow). + * @param {string} scope - Space-separated OAuth2 scopes. Can include project scopes, and built-in scopes: `openid`, `email`, `profile`, `phone`. + * @param {string} state - OAuth2 state. You receive this back in the redirect URI. + * @param {string} nonce - OIDC nonce parameter to prevent replay attacks. Required when response_type includes `id_token`. + * @param {string} codeChallenge - PKCE code challenge. Required when OAuth2 app is public. + * @param {string} codeChallengeMethod - PKCE code challenge method. Required when OAuth2 app is public. + * @param {string} prompt - OIDC prompt parameter for customization of consent screen. Space-separated list of: none, login, consent, select_account. + * @param {number} maxAge - OIDC max_age paraleter for customization of consent screen. Maximum allowable elapsed time in seconds since the user last authenticated. If exceeded, re-authentication is required. + * @param {string} authorizationDetails - Rich authorization request. JSON array of objects, each with a `type` and project-defined fields + * @param {string} resource - RFC 8707 resource indicator URI or URI list. Each value must be an absolute URI without a fragment. + * @param {string} audience - Compatibility alias for a single OAuth2 resource indicator URI. + * @param {string} requestUri - OAuth2 authorization request handle returned by the pushed authorization request endpoint. + * @throws {AppwriteException} + * @returns {Promise} + * @deprecated Use the object parameter style method for a better developer experience. + */ + authorize( + clientId?: string, + redirectUri?: string, + responseType?: string, + scope?: string, + state?: string, + nonce?: string, + codeChallenge?: string, + codeChallengeMethod?: string, + prompt?: string, + maxAge?: number, + authorizationDetails?: string, + resource?: string, + audience?: string, + requestUri?: string, + ): Promise; + authorize( + paramsOrFirst?: + | { + clientId?: string; + redirectUri?: string; + responseType?: string; + scope?: string; + state?: string; + nonce?: string; + codeChallenge?: string; + codeChallengeMethod?: string; + prompt?: string; + maxAge?: number; + authorizationDetails?: string; + resource?: string; + audience?: string; + requestUri?: string; + } + | string, + ...rest: [ + string?, + string?, + string?, + string?, + string?, + string?, + string?, + string?, + number?, + string?, + string?, + string?, + string?, + ] + ): Promise { + let params: { + clientId?: string; + redirectUri?: string; + responseType?: string; + scope?: string; + state?: string; + nonce?: string; + codeChallenge?: string; + codeChallengeMethod?: string; + prompt?: string; + maxAge?: number; + authorizationDetails?: string; + resource?: string; + audience?: string; + requestUri?: string; + }; + + if ( + (typeof paramsOrFirst === 'undefined' && rest.length === 0) || + (paramsOrFirst && + typeof paramsOrFirst === 'object' && + !Array.isArray(paramsOrFirst)) + ) { + params = (paramsOrFirst || {}) as { + clientId?: string; + redirectUri?: string; + responseType?: string; + scope?: string; + state?: string; + nonce?: string; + codeChallenge?: string; + codeChallengeMethod?: string; + prompt?: string; + maxAge?: number; + authorizationDetails?: string; + resource?: string; + audience?: string; + requestUri?: string; + }; + } else { + params = { + clientId: paramsOrFirst as string, + redirectUri: rest[0] as string, + responseType: rest[1] as string, + scope: rest[2] as string, + state: rest[3] as string, + nonce: rest[4] as string, + codeChallenge: rest[5] as string, + codeChallengeMethod: rest[6] as string, + prompt: rest[7] as string, + maxAge: rest[8] as number, + authorizationDetails: rest[9] as string, + resource: rest[10] as string, + audience: rest[11] as string, + requestUri: rest[12] as string, + }; + } + + const clientId = params.clientId; + const redirectUri = params.redirectUri; + const responseType = params.responseType; + const scope = params.scope; + const state = params.state; + const nonce = params.nonce; + const codeChallenge = params.codeChallenge; + const codeChallengeMethod = params.codeChallengeMethod; + const prompt = params.prompt; + const maxAge = params.maxAge; + const authorizationDetails = params.authorizationDetails; + const resource = params.resource; + const audience = params.audience; + const requestUri = params.requestUri; + + const apiPath = '/oauth2/{project_id}/authorize'.replace( + '{project_id}', + encodeURIComponent(String(this.client.config.project)), + ); + const apiPayload: Payload = {}; + + if (typeof clientId !== 'undefined') { + apiPayload['client_id'] = clientId; + } + + if (typeof redirectUri !== 'undefined') { + apiPayload['redirect_uri'] = redirectUri; + } + + if (typeof responseType !== 'undefined') { + apiPayload['response_type'] = responseType; + } + + if (typeof scope !== 'undefined') { + apiPayload['scope'] = scope; + } + + if (typeof state !== 'undefined') { + apiPayload['state'] = state; + } + + if (typeof nonce !== 'undefined') { + apiPayload['nonce'] = nonce; + } + + if (typeof codeChallenge !== 'undefined') { + apiPayload['code_challenge'] = codeChallenge; + } + + if (typeof codeChallengeMethod !== 'undefined') { + apiPayload['code_challenge_method'] = codeChallengeMethod; + } + + if (typeof prompt !== 'undefined') { + apiPayload['prompt'] = prompt; + } + + if (typeof maxAge !== 'undefined') { + apiPayload['max_age'] = maxAge; + } + + if (typeof authorizationDetails !== 'undefined') { + apiPayload['authorization_details'] = authorizationDetails; + } + + if (typeof resource !== 'undefined') { + apiPayload['resource'] = resource; + } + + if (typeof audience !== 'undefined') { + apiPayload['audience'] = audience; + } + + if (typeof requestUri !== 'undefined') { + apiPayload['request_uri'] = requestUri; + } + + const uri = new URL(this.client.config.endpoint + apiPath); + return this.client.call( + 'get', + uri, + { + accept: 'application/json', + }, + apiPayload, + ); + } + + /** + * Begin the OAuth2 authorization flow. When called without a session, the user is redirected to the consent screen without grant ID. When called with a session, the redirect URL includes param for grant ID. You can pass Accept header of `application/json` to receive a JSON response instead of a redirect. + * + * @param {string} params.clientId - OAuth2 client ID. Either a registered app ID or an HTTPS client ID metadata document URL. + * @param {string} params.redirectUri - Redirect URI where visitor will be redirected after authorization, whether successful or not. + * @param {string} params.responseType - OAuth2 / OIDC response type. One of `code` (Authorization Code Flow), `id_token` (Implicit Flow, OIDC login only), or `code id_token` (Hybrid Flow). + * @param {string} params.scope - Space-separated OAuth2 scopes. Can include project scopes, and built-in scopes: `openid`, `email`, `profile`, `phone`. + * @param {string} params.state - OAuth2 state. You receive this back in the redirect URI. + * @param {string} params.nonce - OIDC nonce parameter to prevent replay attacks. Required when response_type includes `id_token`. + * @param {string} params.codeChallenge - PKCE code challenge. Required when OAuth2 app is public. + * @param {string} params.codeChallengeMethod - PKCE code challenge method. Required when OAuth2 app is public. + * @param {string} params.prompt - OIDC prompt parameter for customization of consent screen. Space-separated list of: none, login, consent, select_account. + * @param {number} params.maxAge - OIDC max_age paraleter for customization of consent screen. Maximum allowable elapsed time in seconds since the user last authenticated. If exceeded, re-authentication is required. + * @param {string} params.authorizationDetails - Rich authorization request. JSON array of objects, each with a `type` and project-defined fields + * @param {string} params.resource - RFC 8707 resource indicator URI or URI list. Each value must be an absolute URI without a fragment. + * @param {string} params.audience - Compatibility alias for a single OAuth2 resource indicator URI. + * @param {string} params.requestUri - OAuth2 authorization request handle returned by the pushed authorization request endpoint. + * @throws {AppwriteException} + * @returns {Promise} + */ + authorizePost(params?: { + clientId?: string; + redirectUri?: string; + responseType?: string; + scope?: string; + state?: string; + nonce?: string; + codeChallenge?: string; + codeChallengeMethod?: string; + prompt?: string; + maxAge?: number; + authorizationDetails?: string; + resource?: string; + audience?: string; + requestUri?: string; + }): Promise; + /** + * Begin the OAuth2 authorization flow. When called without a session, the user is redirected to the consent screen without grant ID. When called with a session, the redirect URL includes param for grant ID. You can pass Accept header of `application/json` to receive a JSON response instead of a redirect. + * + * @param {string} clientId - OAuth2 client ID. Either a registered app ID or an HTTPS client ID metadata document URL. + * @param {string} redirectUri - Redirect URI where visitor will be redirected after authorization, whether successful or not. + * @param {string} responseType - OAuth2 / OIDC response type. One of `code` (Authorization Code Flow), `id_token` (Implicit Flow, OIDC login only), or `code id_token` (Hybrid Flow). + * @param {string} scope - Space-separated OAuth2 scopes. Can include project scopes, and built-in scopes: `openid`, `email`, `profile`, `phone`. + * @param {string} state - OAuth2 state. You receive this back in the redirect URI. + * @param {string} nonce - OIDC nonce parameter to prevent replay attacks. Required when response_type includes `id_token`. + * @param {string} codeChallenge - PKCE code challenge. Required when OAuth2 app is public. + * @param {string} codeChallengeMethod - PKCE code challenge method. Required when OAuth2 app is public. + * @param {string} prompt - OIDC prompt parameter for customization of consent screen. Space-separated list of: none, login, consent, select_account. + * @param {number} maxAge - OIDC max_age paraleter for customization of consent screen. Maximum allowable elapsed time in seconds since the user last authenticated. If exceeded, re-authentication is required. + * @param {string} authorizationDetails - Rich authorization request. JSON array of objects, each with a `type` and project-defined fields + * @param {string} resource - RFC 8707 resource indicator URI or URI list. Each value must be an absolute URI without a fragment. + * @param {string} audience - Compatibility alias for a single OAuth2 resource indicator URI. + * @param {string} requestUri - OAuth2 authorization request handle returned by the pushed authorization request endpoint. + * @throws {AppwriteException} + * @returns {Promise} + * @deprecated Use the object parameter style method for a better developer experience. + */ + authorizePost( + clientId?: string, + redirectUri?: string, + responseType?: string, + scope?: string, + state?: string, + nonce?: string, + codeChallenge?: string, + codeChallengeMethod?: string, + prompt?: string, + maxAge?: number, + authorizationDetails?: string, + resource?: string, + audience?: string, + requestUri?: string, + ): Promise; + authorizePost( + paramsOrFirst?: + | { + clientId?: string; + redirectUri?: string; + responseType?: string; + scope?: string; + state?: string; + nonce?: string; + codeChallenge?: string; + codeChallengeMethod?: string; + prompt?: string; + maxAge?: number; + authorizationDetails?: string; + resource?: string; + audience?: string; + requestUri?: string; + } + | string, + ...rest: [ + string?, + string?, + string?, + string?, + string?, + string?, + string?, + string?, + number?, + string?, + string?, + string?, + string?, + ] + ): Promise { + let params: { + clientId?: string; + redirectUri?: string; + responseType?: string; + scope?: string; + state?: string; + nonce?: string; + codeChallenge?: string; + codeChallengeMethod?: string; + prompt?: string; + maxAge?: number; + authorizationDetails?: string; + resource?: string; + audience?: string; + requestUri?: string; + }; + + if ( + (typeof paramsOrFirst === 'undefined' && rest.length === 0) || + (paramsOrFirst && + typeof paramsOrFirst === 'object' && + !Array.isArray(paramsOrFirst)) + ) { + params = (paramsOrFirst || {}) as { + clientId?: string; + redirectUri?: string; + responseType?: string; + scope?: string; + state?: string; + nonce?: string; + codeChallenge?: string; + codeChallengeMethod?: string; + prompt?: string; + maxAge?: number; + authorizationDetails?: string; + resource?: string; + audience?: string; + requestUri?: string; + }; + } else { + params = { + clientId: paramsOrFirst as string, + redirectUri: rest[0] as string, + responseType: rest[1] as string, + scope: rest[2] as string, + state: rest[3] as string, + nonce: rest[4] as string, + codeChallenge: rest[5] as string, + codeChallengeMethod: rest[6] as string, + prompt: rest[7] as string, + maxAge: rest[8] as number, + authorizationDetails: rest[9] as string, + resource: rest[10] as string, + audience: rest[11] as string, + requestUri: rest[12] as string, + }; + } + + const clientId = params.clientId; + const redirectUri = params.redirectUri; + const responseType = params.responseType; + const scope = params.scope; + const state = params.state; + const nonce = params.nonce; + const codeChallenge = params.codeChallenge; + const codeChallengeMethod = params.codeChallengeMethod; + const prompt = params.prompt; + const maxAge = params.maxAge; + const authorizationDetails = params.authorizationDetails; + const resource = params.resource; + const audience = params.audience; + const requestUri = params.requestUri; + + const apiPath = '/oauth2/{project_id}/authorize'.replace( + '{project_id}', + encodeURIComponent(String(this.client.config.project)), + ); + const apiPayload: Payload = {}; + + if (typeof clientId !== 'undefined') { + apiPayload['client_id'] = clientId; + } + + if (typeof redirectUri !== 'undefined') { + apiPayload['redirect_uri'] = redirectUri; + } + + if (typeof responseType !== 'undefined') { + apiPayload['response_type'] = responseType; + } + + if (typeof scope !== 'undefined') { + apiPayload['scope'] = scope; + } + + if (typeof state !== 'undefined') { + apiPayload['state'] = state; + } + + if (typeof nonce !== 'undefined') { + apiPayload['nonce'] = nonce; + } + + if (typeof codeChallenge !== 'undefined') { + apiPayload['code_challenge'] = codeChallenge; + } + + if (typeof codeChallengeMethod !== 'undefined') { + apiPayload['code_challenge_method'] = codeChallengeMethod; + } + + if (typeof prompt !== 'undefined') { + apiPayload['prompt'] = prompt; + } + + if (typeof maxAge !== 'undefined') { + apiPayload['max_age'] = maxAge; + } + + if (typeof authorizationDetails !== 'undefined') { + apiPayload['authorization_details'] = authorizationDetails; + } + + if (typeof resource !== 'undefined') { + apiPayload['resource'] = resource; + } + + if (typeof audience !== 'undefined') { + apiPayload['audience'] = audience; + } + + if (typeof requestUri !== 'undefined') { + apiPayload['request_uri'] = requestUri; + } + + const uri = new URL(this.client.config.endpoint + apiPath); + return this.client.call( + 'post', + uri, + { + 'content-type': 'application/json', + accept: 'application/json', + }, + apiPayload, + ); + } + + /** + * Start the OAuth2 Device Authorization Grant. Returns the device code, user code, verification URL, expiration, and polling interval. + * + * @param {string} params.clientId - OAuth2 client ID. Either a registered app ID or an HTTPS client ID metadata document URL. + * @param {string} params.scope - Space-separated OAuth2 scopes. Can include project scopes, and built-in scopes: `openid`, `email`, `profile`. + * @param {string} params.authorizationDetails - Rich authorization request. JSON array of objects, each with a `type` and project-defined fields + * @param {string} params.resource - RFC 8707 resource indicator URI or URI list. Each value must be an absolute URI without a fragment. + * @param {string} params.audience - Compatibility alias for a single OAuth2 resource indicator URI. + * @throws {AppwriteException} + * @returns {Promise} + */ + createDeviceAuthorization(params?: { + clientId?: string; + scope?: string; + authorizationDetails?: string; + resource?: string; + audience?: string; + }): Promise; + /** + * Start the OAuth2 Device Authorization Grant. Returns the device code, user code, verification URL, expiration, and polling interval. + * + * @param {string} clientId - OAuth2 client ID. Either a registered app ID or an HTTPS client ID metadata document URL. + * @param {string} scope - Space-separated OAuth2 scopes. Can include project scopes, and built-in scopes: `openid`, `email`, `profile`. + * @param {string} authorizationDetails - Rich authorization request. JSON array of objects, each with a `type` and project-defined fields + * @param {string} resource - RFC 8707 resource indicator URI or URI list. Each value must be an absolute URI without a fragment. + * @param {string} audience - Compatibility alias for a single OAuth2 resource indicator URI. + * @throws {AppwriteException} + * @returns {Promise} + * @deprecated Use the object parameter style method for a better developer experience. + */ + createDeviceAuthorization( + clientId?: string, + scope?: string, + authorizationDetails?: string, + resource?: string, + audience?: string, + ): Promise; + createDeviceAuthorization( + paramsOrFirst?: + | { + clientId?: string; + scope?: string; + authorizationDetails?: string; + resource?: string; + audience?: string; + } + | string, + ...rest: [string?, string?, string?, string?] + ): Promise { + let params: { + clientId?: string; + scope?: string; + authorizationDetails?: string; + resource?: string; + audience?: string; + }; + + if ( + (typeof paramsOrFirst === 'undefined' && rest.length === 0) || + (paramsOrFirst && + typeof paramsOrFirst === 'object' && + !Array.isArray(paramsOrFirst)) + ) { + params = (paramsOrFirst || {}) as { + clientId?: string; + scope?: string; + authorizationDetails?: string; + resource?: string; + audience?: string; + }; + } else { + params = { + clientId: paramsOrFirst as string, + scope: rest[0] as string, + authorizationDetails: rest[1] as string, + resource: rest[2] as string, + audience: rest[3] as string, + }; + } + + const clientId = params.clientId; + const scope = params.scope; + const authorizationDetails = params.authorizationDetails; + const resource = params.resource; + const audience = params.audience; + + const apiPath = '/oauth2/{project_id}/device_authorization'.replace( + '{project_id}', + encodeURIComponent(String(this.client.config.project)), + ); + const apiPayload: Payload = {}; + + if (typeof clientId !== 'undefined') { + apiPayload['client_id'] = clientId; + } + + if (typeof scope !== 'undefined') { + apiPayload['scope'] = scope; + } + + if (typeof authorizationDetails !== 'undefined') { + apiPayload['authorization_details'] = authorizationDetails; + } + + if (typeof resource !== 'undefined') { + apiPayload['resource'] = resource; + } + + if (typeof audience !== 'undefined') { + apiPayload['audience'] = audience; + } + + const uri = new URL(this.client.config.endpoint + apiPath); + return this.client.call( + 'post', + uri, + { + 'content-type': 'application/json', + accept: 'application/json', + }, + apiPayload, + ); + } + + /** + * Exchange a device flow user code for an OAuth2 grant. The authenticated user is bound to the pending grant. Pass the returned grant ID to the get grant endpoint to render the consent screen, then to the approve or reject endpoint to complete the flow. + * + * @param {string} params.userCode - User code displayed on the device. + * @throws {AppwriteException} + * @returns {Promise} + */ + createGrant(params: { userCode: string }): Promise; + /** + * Exchange a device flow user code for an OAuth2 grant. The authenticated user is bound to the pending grant. Pass the returned grant ID to the get grant endpoint to render the consent screen, then to the approve or reject endpoint to complete the flow. + * + * @param {string} userCode - User code displayed on the device. + * @throws {AppwriteException} + * @returns {Promise} + * @deprecated Use the object parameter style method for a better developer experience. + */ + createGrant(userCode: string): Promise; + createGrant( + paramsOrFirst: { userCode: string } | string, + ): Promise { + let params: { userCode: string }; + + if ( + paramsOrFirst && + typeof paramsOrFirst === 'object' && + !Array.isArray(paramsOrFirst) + ) { + params = (paramsOrFirst || {}) as { userCode: string }; + } else { + params = { + userCode: paramsOrFirst as string, + }; + } + + const userCode = params.userCode; + + if (typeof userCode === 'undefined') { + throw new AppwriteException( + 'Missing required parameter: "userCode"', + ); + } + + const apiPath = '/oauth2/{project_id}/grants'.replace( + '{project_id}', + encodeURIComponent(String(this.client.config.project)), + ); + const apiPayload: Payload = {}; + + if (typeof userCode !== 'undefined') { + apiPayload['user_code'] = userCode; + } + + const uri = new URL(this.client.config.endpoint + apiPath); + return this.client.call( + 'post', + uri, + { + 'content-type': 'application/json', + accept: 'application/json', + }, + apiPayload, + ); + } + + /** + * Get an OAuth2 grant by its ID. Used by the consent screen to display the details of the authorization the user is being asked to approve. A grant can only be read by the user it belongs to, or by server SDK. + * + * @param {string} params.grantId - Grant ID made during authorization, provided to consent screen in URL search params. + * @throws {AppwriteException} + * @returns {Promise} + */ + getGrant(params: { grantId: string }): Promise; + /** + * Get an OAuth2 grant by its ID. Used by the consent screen to display the details of the authorization the user is being asked to approve. A grant can only be read by the user it belongs to, or by server SDK. + * + * @param {string} grantId - Grant ID made during authorization, provided to consent screen in URL search params. + * @throws {AppwriteException} + * @returns {Promise} + * @deprecated Use the object parameter style method for a better developer experience. + */ + getGrant(grantId: string): Promise; + getGrant( + paramsOrFirst: { grantId: string } | string, + ): Promise { + let params: { grantId: string }; + + if ( + paramsOrFirst && + typeof paramsOrFirst === 'object' && + !Array.isArray(paramsOrFirst) + ) { + params = (paramsOrFirst || {}) as { grantId: string }; + } else { + params = { + grantId: paramsOrFirst as string, + }; + } + + const grantId = params.grantId; + + if (typeof grantId === 'undefined' || grantId === '') { + throw new AppwriteException( + 'Missing required parameter: "grantId"', + ); + } + + const apiPath = '/oauth2/{project_id}/grants/{grant_id}' + .replace( + '{project_id}', + encodeURIComponent(String(this.client.config.project)), + ) + .replace('{grant_id}', encodeURIComponent(String(grantId))); + const apiPayload: Payload = {}; + + const uri = new URL(this.client.config.endpoint + apiPath); + return this.client.call( + 'get', + uri, + { + accept: 'application/json', + }, + apiPayload, + ); + } + + /** + * OpenID Connect RP-Initiated Logout. Ends the user session and revokes the tokens issued to the app identified by the `id_token_hint`, then redirects the user to `post_logout_redirect_uri` when it matches a URI registered on the app. + * + * @param {string} params.idTokenHint - ID Token previously issued to the app, used as proof of the logout request. Required to end the session; signature and issuer are validated while expiry is ignored. + * @param {string} params.logoutHint - Hint about the user that is logging out. Accepted for OIDC compatibility. + * @param {string} params.clientId - OAuth2 client ID. When both `client_id` and `id_token_hint` are provided, they must identify the same app. + * @param {string} params.postLogoutRedirectUri - URI to redirect the user to after logout. Must exactly match a URI registered in the app's `postLogoutRedirectUris`. + * @param {string} params.state - Opaque value passed back unchanged in the `state` query param of the post-logout redirect. + * @param {string} params.uiLocales - Preferred languages for any logout UI, as space-separated BCP47 tags. Accepted for OIDC compatibility. + * @throws {AppwriteException} + * @returns {Promise} + */ + logout(params?: { + idTokenHint?: string; + logoutHint?: string; + clientId?: string; + postLogoutRedirectUri?: string; + state?: string; + uiLocales?: string; + }): Promise<{}>; + /** + * OpenID Connect RP-Initiated Logout. Ends the user session and revokes the tokens issued to the app identified by the `id_token_hint`, then redirects the user to `post_logout_redirect_uri` when it matches a URI registered on the app. + * + * @param {string} idTokenHint - ID Token previously issued to the app, used as proof of the logout request. Required to end the session; signature and issuer are validated while expiry is ignored. + * @param {string} logoutHint - Hint about the user that is logging out. Accepted for OIDC compatibility. + * @param {string} clientId - OAuth2 client ID. When both `client_id` and `id_token_hint` are provided, they must identify the same app. + * @param {string} postLogoutRedirectUri - URI to redirect the user to after logout. Must exactly match a URI registered in the app's `postLogoutRedirectUris`. + * @param {string} state - Opaque value passed back unchanged in the `state` query param of the post-logout redirect. + * @param {string} uiLocales - Preferred languages for any logout UI, as space-separated BCP47 tags. Accepted for OIDC compatibility. + * @throws {AppwriteException} + * @returns {Promise<{}>} + * @deprecated Use the object parameter style method for a better developer experience. + */ + logout( + idTokenHint?: string, + logoutHint?: string, + clientId?: string, + postLogoutRedirectUri?: string, + state?: string, + uiLocales?: string, + ): Promise<{}>; + logout( + paramsOrFirst?: + | { + idTokenHint?: string; + logoutHint?: string; + clientId?: string; + postLogoutRedirectUri?: string; + state?: string; + uiLocales?: string; + } + | string, + ...rest: [string?, string?, string?, string?, string?] + ): Promise<{}> { + let params: { + idTokenHint?: string; + logoutHint?: string; + clientId?: string; + postLogoutRedirectUri?: string; + state?: string; + uiLocales?: string; + }; + + if ( + (typeof paramsOrFirst === 'undefined' && rest.length === 0) || + (paramsOrFirst && + typeof paramsOrFirst === 'object' && + !Array.isArray(paramsOrFirst)) + ) { + params = (paramsOrFirst || {}) as { + idTokenHint?: string; + logoutHint?: string; + clientId?: string; + postLogoutRedirectUri?: string; + state?: string; + uiLocales?: string; + }; + } else { + params = { + idTokenHint: paramsOrFirst as string, + logoutHint: rest[0] as string, + clientId: rest[1] as string, + postLogoutRedirectUri: rest[2] as string, + state: rest[3] as string, + uiLocales: rest[4] as string, + }; + } + + const idTokenHint = params.idTokenHint; + const logoutHint = params.logoutHint; + const clientId = params.clientId; + const postLogoutRedirectUri = params.postLogoutRedirectUri; + const state = params.state; + const uiLocales = params.uiLocales; + + const apiPath = '/oauth2/{project_id}/logout'.replace( + '{project_id}', + encodeURIComponent(String(this.client.config.project)), + ); + const apiPayload: Payload = {}; + + if (typeof idTokenHint !== 'undefined') { + apiPayload['id_token_hint'] = idTokenHint; + } + + if (typeof logoutHint !== 'undefined') { + apiPayload['logout_hint'] = logoutHint; + } + + if (typeof clientId !== 'undefined') { + apiPayload['client_id'] = clientId; + } + + if (typeof postLogoutRedirectUri !== 'undefined') { + apiPayload['post_logout_redirect_uri'] = postLogoutRedirectUri; + } + + if (typeof state !== 'undefined') { + apiPayload['state'] = state; + } + + if (typeof uiLocales !== 'undefined') { + apiPayload['ui_locales'] = uiLocales; + } + + const uri = new URL(this.client.config.endpoint + apiPath); + return this.client.call( + 'get', + uri, + { + accept: 'application/json', + }, + apiPayload, + ); + } + + /** + * OpenID Connect RP-Initiated Logout. Ends the user session and revokes the tokens issued to the app identified by the `id_token_hint`, then redirects the user to `post_logout_redirect_uri` when it matches a URI registered on the app. + * + * @param {string} params.idTokenHint - ID Token previously issued to the app, used as proof of the logout request. Required to end the session; signature and issuer are validated while expiry is ignored. + * @param {string} params.logoutHint - Hint about the user that is logging out. Accepted for OIDC compatibility. + * @param {string} params.clientId - OAuth2 client ID. When both `client_id` and `id_token_hint` are provided, they must identify the same app. + * @param {string} params.postLogoutRedirectUri - URI to redirect the user to after logout. Must exactly match a URI registered in the app's `postLogoutRedirectUris`. + * @param {string} params.state - Opaque value passed back unchanged in the `state` query param of the post-logout redirect. + * @param {string} params.uiLocales - Preferred languages for any logout UI, as space-separated BCP47 tags. Accepted for OIDC compatibility. + * @throws {AppwriteException} + * @returns {Promise} + */ + logoutPost(params?: { + idTokenHint?: string; + logoutHint?: string; + clientId?: string; + postLogoutRedirectUri?: string; + state?: string; + uiLocales?: string; + }): Promise<{}>; + /** + * OpenID Connect RP-Initiated Logout. Ends the user session and revokes the tokens issued to the app identified by the `id_token_hint`, then redirects the user to `post_logout_redirect_uri` when it matches a URI registered on the app. + * + * @param {string} idTokenHint - ID Token previously issued to the app, used as proof of the logout request. Required to end the session; signature and issuer are validated while expiry is ignored. + * @param {string} logoutHint - Hint about the user that is logging out. Accepted for OIDC compatibility. + * @param {string} clientId - OAuth2 client ID. When both `client_id` and `id_token_hint` are provided, they must identify the same app. + * @param {string} postLogoutRedirectUri - URI to redirect the user to after logout. Must exactly match a URI registered in the app's `postLogoutRedirectUris`. + * @param {string} state - Opaque value passed back unchanged in the `state` query param of the post-logout redirect. + * @param {string} uiLocales - Preferred languages for any logout UI, as space-separated BCP47 tags. Accepted for OIDC compatibility. + * @throws {AppwriteException} + * @returns {Promise<{}>} + * @deprecated Use the object parameter style method for a better developer experience. + */ + logoutPost( + idTokenHint?: string, + logoutHint?: string, + clientId?: string, + postLogoutRedirectUri?: string, + state?: string, + uiLocales?: string, + ): Promise<{}>; + logoutPost( + paramsOrFirst?: + | { + idTokenHint?: string; + logoutHint?: string; + clientId?: string; + postLogoutRedirectUri?: string; + state?: string; + uiLocales?: string; + } + | string, + ...rest: [string?, string?, string?, string?, string?] + ): Promise<{}> { + let params: { + idTokenHint?: string; + logoutHint?: string; + clientId?: string; + postLogoutRedirectUri?: string; + state?: string; + uiLocales?: string; + }; + + if ( + (typeof paramsOrFirst === 'undefined' && rest.length === 0) || + (paramsOrFirst && + typeof paramsOrFirst === 'object' && + !Array.isArray(paramsOrFirst)) + ) { + params = (paramsOrFirst || {}) as { + idTokenHint?: string; + logoutHint?: string; + clientId?: string; + postLogoutRedirectUri?: string; + state?: string; + uiLocales?: string; + }; + } else { + params = { + idTokenHint: paramsOrFirst as string, + logoutHint: rest[0] as string, + clientId: rest[1] as string, + postLogoutRedirectUri: rest[2] as string, + state: rest[3] as string, + uiLocales: rest[4] as string, + }; + } + + const idTokenHint = params.idTokenHint; + const logoutHint = params.logoutHint; + const clientId = params.clientId; + const postLogoutRedirectUri = params.postLogoutRedirectUri; + const state = params.state; + const uiLocales = params.uiLocales; + + const apiPath = '/oauth2/{project_id}/logout'.replace( + '{project_id}', + encodeURIComponent(String(this.client.config.project)), + ); + const apiPayload: Payload = {}; + + if (typeof idTokenHint !== 'undefined') { + apiPayload['id_token_hint'] = idTokenHint; + } + + if (typeof logoutHint !== 'undefined') { + apiPayload['logout_hint'] = logoutHint; + } + + if (typeof clientId !== 'undefined') { + apiPayload['client_id'] = clientId; + } + + if (typeof postLogoutRedirectUri !== 'undefined') { + apiPayload['post_logout_redirect_uri'] = postLogoutRedirectUri; + } + + if (typeof state !== 'undefined') { + apiPayload['state'] = state; + } + + if (typeof uiLocales !== 'undefined') { + apiPayload['ui_locales'] = uiLocales; + } + + const uri = new URL(this.client.config.endpoint + apiPath); + return this.client.call( + 'post', + uri, + { + 'content-type': 'application/json', + accept: 'application/json', + }, + apiPayload, + ); + } + + /** + * List the organizations the OAuth2 access token can access. Resolves the token's `organization` authorization details, expanding the `*` wildcard into the concrete set of organizations the user can see. + * + * @param {number} params.limit - Maximum number of organizations to return. Between 1 and 5000. + * @param {number} params.offset - Number of organizations to skip before returning results. Used for pagination. + * @param {string} params.search - Search term to filter your list results. Max length: 256 chars. + * @throws {AppwriteException} + * @returns {Promise} + */ + listOrganizations(params?: { + limit?: number; + offset?: number; + search?: string; + }): Promise; + /** + * List the organizations the OAuth2 access token can access. Resolves the token's `organization` authorization details, expanding the `*` wildcard into the concrete set of organizations the user can see. + * + * @param {number} limit - Maximum number of organizations to return. Between 1 and 5000. + * @param {number} offset - Number of organizations to skip before returning results. Used for pagination. + * @param {string} search - Search term to filter your list results. Max length: 256 chars. + * @throws {AppwriteException} + * @returns {Promise} + * @deprecated Use the object parameter style method for a better developer experience. + */ + listOrganizations( + limit?: number, + offset?: number, + search?: string, + ): Promise; + listOrganizations( + paramsOrFirst?: + { limit?: number; offset?: number; search?: string } | number, + ...rest: [number?, string?] + ): Promise { + let params: { limit?: number; offset?: number; search?: string }; + + if ( + (typeof paramsOrFirst === 'undefined' && rest.length === 0) || + (paramsOrFirst && + typeof paramsOrFirst === 'object' && + !Array.isArray(paramsOrFirst)) + ) { + params = (paramsOrFirst || {}) as { + limit?: number; + offset?: number; + search?: string; + }; + } else { + params = { + limit: paramsOrFirst as number, + offset: rest[0] as number, + search: rest[1] as string, + }; + } + + const limit = params.limit; + const offset = params.offset; + const search = params.search; + + const apiPath = '/oauth2/{project_id}/organizations'.replace( + '{project_id}', + encodeURIComponent(String(this.client.config.project)), + ); + const apiPayload: Payload = {}; + + if (typeof limit !== 'undefined') { + apiPayload['limit'] = limit; + } + + if (typeof offset !== 'undefined') { + apiPayload['offset'] = offset; + } + + if (typeof search !== 'undefined') { + apiPayload['search'] = search; + } + + const uri = new URL(this.client.config.endpoint + apiPath); + return this.client.call( + 'get', + uri, + { + accept: 'application/json', + }, + apiPayload, + ); + } + + /** + * Store an OAuth2 authorization request server-side and receive a short-lived request_uri handle for the authorize endpoint. + * + * @param {string} params.clientId - OAuth2 client ID. Either a registered app ID or an HTTPS client ID metadata document URL. + * @param {string} params.redirectUri - Redirect URI where visitor will be redirected after authorization, whether successful or not. + * @param {string} params.responseType - OAuth2 / OIDC response type. + * @param {string} params.scope - Space-separated OAuth2 scopes. Can include project scopes, and built-in scopes: `openid`, `email`, `profile`, `phone`. + * @param {string} params.state - OAuth2 state. You receive this back in the redirect URI. + * @param {string} params.nonce - OIDC nonce parameter to prevent replay attacks. Required when response_type includes `id_token`. + * @param {string} params.codeChallenge - PKCE code challenge. Required when OAuth2 app is public. + * @param {string} params.codeChallengeMethod - PKCE code challenge method. Required when OAuth2 app is public. + * @param {string} params.prompt - OIDC prompt parameter for customization of consent screen. Space-separated list of: none, login, consent, select_account. + * @param {number} params.maxAge - OIDC max_age parameter for customization of consent screen. + * @param {string} params.authorizationDetails - Rich authorization request. JSON array of objects, each with a `type` and project-defined fields + * @param {string} params.resource - RFC 8707 resource indicator URI or URI list. Each value must be an absolute URI without a fragment. + * @param {string} params.audience - Compatibility alias for a single OAuth2 resource indicator URI. + * @throws {AppwriteException} + * @returns {Promise} + */ + createPAR(params: { + clientId: string; + redirectUri: string; + responseType: string; + scope?: string; + state?: string; + nonce?: string; + codeChallenge?: string; + codeChallengeMethod?: string; + prompt?: string; + maxAge?: number; + authorizationDetails?: string; + resource?: string; + audience?: string; + }): Promise; + /** + * Store an OAuth2 authorization request server-side and receive a short-lived request_uri handle for the authorize endpoint. + * + * @param {string} clientId - OAuth2 client ID. Either a registered app ID or an HTTPS client ID metadata document URL. + * @param {string} redirectUri - Redirect URI where visitor will be redirected after authorization, whether successful or not. + * @param {string} responseType - OAuth2 / OIDC response type. + * @param {string} scope - Space-separated OAuth2 scopes. Can include project scopes, and built-in scopes: `openid`, `email`, `profile`, `phone`. + * @param {string} state - OAuth2 state. You receive this back in the redirect URI. + * @param {string} nonce - OIDC nonce parameter to prevent replay attacks. Required when response_type includes `id_token`. + * @param {string} codeChallenge - PKCE code challenge. Required when OAuth2 app is public. + * @param {string} codeChallengeMethod - PKCE code challenge method. Required when OAuth2 app is public. + * @param {string} prompt - OIDC prompt parameter for customization of consent screen. Space-separated list of: none, login, consent, select_account. + * @param {number} maxAge - OIDC max_age parameter for customization of consent screen. + * @param {string} authorizationDetails - Rich authorization request. JSON array of objects, each with a `type` and project-defined fields + * @param {string} resource - RFC 8707 resource indicator URI or URI list. Each value must be an absolute URI without a fragment. + * @param {string} audience - Compatibility alias for a single OAuth2 resource indicator URI. + * @throws {AppwriteException} + * @returns {Promise} + * @deprecated Use the object parameter style method for a better developer experience. + */ + createPAR( + clientId: string, + redirectUri: string, + responseType: string, + scope?: string, + state?: string, + nonce?: string, + codeChallenge?: string, + codeChallengeMethod?: string, + prompt?: string, + maxAge?: number, + authorizationDetails?: string, + resource?: string, + audience?: string, + ): Promise; + createPAR( + paramsOrFirst: + | { + clientId: string; + redirectUri: string; + responseType: string; + scope?: string; + state?: string; + nonce?: string; + codeChallenge?: string; + codeChallengeMethod?: string; + prompt?: string; + maxAge?: number; + authorizationDetails?: string; + resource?: string; + audience?: string; + } + | string, + ...rest: [ + string?, + string?, + string?, + string?, + string?, + string?, + string?, + string?, + number?, + string?, + string?, + string?, + ] + ): Promise { + let params: { + clientId: string; + redirectUri: string; + responseType: string; + scope?: string; + state?: string; + nonce?: string; + codeChallenge?: string; + codeChallengeMethod?: string; + prompt?: string; + maxAge?: number; + authorizationDetails?: string; + resource?: string; + audience?: string; + }; + + if ( + paramsOrFirst && + typeof paramsOrFirst === 'object' && + !Array.isArray(paramsOrFirst) + ) { + params = (paramsOrFirst || {}) as { + clientId: string; + redirectUri: string; + responseType: string; + scope?: string; + state?: string; + nonce?: string; + codeChallenge?: string; + codeChallengeMethod?: string; + prompt?: string; + maxAge?: number; + authorizationDetails?: string; + resource?: string; + audience?: string; + }; + } else { + params = { + clientId: paramsOrFirst as string, + redirectUri: rest[0] as string, + responseType: rest[1] as string, + scope: rest[2] as string, + state: rest[3] as string, + nonce: rest[4] as string, + codeChallenge: rest[5] as string, + codeChallengeMethod: rest[6] as string, + prompt: rest[7] as string, + maxAge: rest[8] as number, + authorizationDetails: rest[9] as string, + resource: rest[10] as string, + audience: rest[11] as string, + }; + } + + const clientId = params.clientId; + const redirectUri = params.redirectUri; + const responseType = params.responseType; + const scope = params.scope; + const state = params.state; + const nonce = params.nonce; + const codeChallenge = params.codeChallenge; + const codeChallengeMethod = params.codeChallengeMethod; + const prompt = params.prompt; + const maxAge = params.maxAge; + const authorizationDetails = params.authorizationDetails; + const resource = params.resource; + const audience = params.audience; + + if (typeof clientId === 'undefined') { + throw new AppwriteException( + 'Missing required parameter: "clientId"', + ); + } + + if (typeof redirectUri === 'undefined') { + throw new AppwriteException( + 'Missing required parameter: "redirectUri"', + ); + } + + if (typeof responseType === 'undefined') { + throw new AppwriteException( + 'Missing required parameter: "responseType"', + ); + } + + const apiPath = '/oauth2/{project_id}/par'.replace( + '{project_id}', + encodeURIComponent(String(this.client.config.project)), + ); + const apiPayload: Payload = {}; + + if (typeof clientId !== 'undefined') { + apiPayload['client_id'] = clientId; + } + + if (typeof redirectUri !== 'undefined') { + apiPayload['redirect_uri'] = redirectUri; + } + + if (typeof responseType !== 'undefined') { + apiPayload['response_type'] = responseType; + } + + if (typeof scope !== 'undefined') { + apiPayload['scope'] = scope; + } + + if (typeof state !== 'undefined') { + apiPayload['state'] = state; + } + + if (typeof nonce !== 'undefined') { + apiPayload['nonce'] = nonce; + } + + if (typeof codeChallenge !== 'undefined') { + apiPayload['code_challenge'] = codeChallenge; + } + + if (typeof codeChallengeMethod !== 'undefined') { + apiPayload['code_challenge_method'] = codeChallengeMethod; + } + + if (typeof prompt !== 'undefined') { + apiPayload['prompt'] = prompt; + } + + if (typeof maxAge !== 'undefined') { + apiPayload['max_age'] = maxAge; + } + + if (typeof authorizationDetails !== 'undefined') { + apiPayload['authorization_details'] = authorizationDetails; + } + + if (typeof resource !== 'undefined') { + apiPayload['resource'] = resource; + } + + if (typeof audience !== 'undefined') { + apiPayload['audience'] = audience; + } + + const uri = new URL(this.client.config.endpoint + apiPath); + return this.client.call( + 'post', + uri, + { + 'content-type': 'application/json', + accept: 'application/json', + }, + apiPayload, + ); + } + + /** + * List the projects the OAuth2 access token can access. Resolves the token's `project` authorization details, expanding the `*` wildcard into the concrete set of projects the user can see. + * + * @param {number} params.limit - Maximum number of projects to return. Between 1 and 5000. + * @param {number} params.offset - Number of projects to skip before returning results. Used for pagination. + * @param {string} params.search - Search term to filter your list results. Max length: 256 chars. + * @throws {AppwriteException} + * @returns {Promise} + */ + listProjects(params?: { + limit?: number; + offset?: number; + search?: string; + }): Promise; + /** + * List the projects the OAuth2 access token can access. Resolves the token's `project` authorization details, expanding the `*` wildcard into the concrete set of projects the user can see. + * + * @param {number} limit - Maximum number of projects to return. Between 1 and 5000. + * @param {number} offset - Number of projects to skip before returning results. Used for pagination. + * @param {string} search - Search term to filter your list results. Max length: 256 chars. + * @throws {AppwriteException} + * @returns {Promise} + * @deprecated Use the object parameter style method for a better developer experience. + */ + listProjects( + limit?: number, + offset?: number, + search?: string, + ): Promise; + listProjects( + paramsOrFirst?: + { limit?: number; offset?: number; search?: string } | number, + ...rest: [number?, string?] + ): Promise { + let params: { limit?: number; offset?: number; search?: string }; + + if ( + (typeof paramsOrFirst === 'undefined' && rest.length === 0) || + (paramsOrFirst && + typeof paramsOrFirst === 'object' && + !Array.isArray(paramsOrFirst)) + ) { + params = (paramsOrFirst || {}) as { + limit?: number; + offset?: number; + search?: string; + }; + } else { + params = { + limit: paramsOrFirst as number, + offset: rest[0] as number, + search: rest[1] as string, + }; + } + + const limit = params.limit; + const offset = params.offset; + const search = params.search; + + const apiPath = '/oauth2/{project_id}/projects'.replace( + '{project_id}', + encodeURIComponent(String(this.client.config.project)), + ); + const apiPayload: Payload = {}; + + if (typeof limit !== 'undefined') { + apiPayload['limit'] = limit; + } + + if (typeof offset !== 'undefined') { + apiPayload['offset'] = offset; + } + + if (typeof search !== 'undefined') { + apiPayload['search'] = search; + } + + const uri = new URL(this.client.config.endpoint + apiPath); + return this.client.call( + 'get', + uri, + { + accept: 'application/json', + }, + apiPayload, + ); + } + + /** + * Reject an OAuth2 grant when the user denies consent. Returns the `redirectUrl` the end user should be sent to with an `access_denied` error. You can pass Accept header of `application/json` to receive a JSON response instead of a redirect. + * + * @param {string} params.grantId - Grant ID made during authorization, provided to consent screen in URL search params. + * @throws {AppwriteException} + * @returns {Promise} + */ + reject(params: { grantId: string }): Promise; + /** + * Reject an OAuth2 grant when the user denies consent. Returns the `redirectUrl` the end user should be sent to with an `access_denied` error. You can pass Accept header of `application/json` to receive a JSON response instead of a redirect. + * + * @param {string} grantId - Grant ID made during authorization, provided to consent screen in URL search params. + * @throws {AppwriteException} + * @returns {Promise} + * @deprecated Use the object parameter style method for a better developer experience. + */ + reject(grantId: string): Promise; + reject( + paramsOrFirst: { grantId: string } | string, + ): Promise { + let params: { grantId: string }; + + if ( + paramsOrFirst && + typeof paramsOrFirst === 'object' && + !Array.isArray(paramsOrFirst) + ) { + params = (paramsOrFirst || {}) as { grantId: string }; + } else { + params = { + grantId: paramsOrFirst as string, + }; + } + + const grantId = params.grantId; + + if (typeof grantId === 'undefined') { + throw new AppwriteException( + 'Missing required parameter: "grantId"', + ); + } + + const apiPath = '/oauth2/{project_id}/reject'.replace( + '{project_id}', + encodeURIComponent(String(this.client.config.project)), + ); + const apiPayload: Payload = {}; + + if (typeof grantId !== 'undefined') { + apiPayload['grant_id'] = grantId; + } + + const uri = new URL(this.client.config.endpoint + apiPath); + return this.client.call( + 'post', + uri, + { + 'content-type': 'application/json', + accept: 'application/json', + }, + apiPayload, + ); + } + + /** + * Revoke an OAuth2 access token or refresh token. + * + * @param {string} params.token - The access or refresh token to revoke. + * @param {string} params.tokenTypeHint - Type of token to revoke (access_token or refresh_token). + * @param {string} params.clientId - OAuth2 client ID. Either a registered app ID or an HTTPS client ID metadata document URL. + * @param {string} params.clientSecret - OAuth2 client secret. Required for confidential apps; omitted for public apps. + * @throws {AppwriteException} + * @returns {Promise} + */ + revoke(params: { + token: string; + tokenTypeHint?: string; + clientId?: string; + clientSecret?: string; + }): Promise<{}>; + /** + * Revoke an OAuth2 access token or refresh token. + * + * @param {string} token - The access or refresh token to revoke. + * @param {string} tokenTypeHint - Type of token to revoke (access_token or refresh_token). + * @param {string} clientId - OAuth2 client ID. Either a registered app ID or an HTTPS client ID metadata document URL. + * @param {string} clientSecret - OAuth2 client secret. Required for confidential apps; omitted for public apps. + * @throws {AppwriteException} + * @returns {Promise<{}>} + * @deprecated Use the object parameter style method for a better developer experience. + */ + revoke( + token: string, + tokenTypeHint?: string, + clientId?: string, + clientSecret?: string, + ): Promise<{}>; + revoke( + paramsOrFirst: + | { + token: string; + tokenTypeHint?: string; + clientId?: string; + clientSecret?: string; + } + | string, + ...rest: [string?, string?, string?] + ): Promise<{}> { + let params: { + token: string; + tokenTypeHint?: string; + clientId?: string; + clientSecret?: string; + }; + + if ( + paramsOrFirst && + typeof paramsOrFirst === 'object' && + !Array.isArray(paramsOrFirst) + ) { + params = (paramsOrFirst || {}) as { + token: string; + tokenTypeHint?: string; + clientId?: string; + clientSecret?: string; + }; + } else { + params = { + token: paramsOrFirst as string, + tokenTypeHint: rest[0] as string, + clientId: rest[1] as string, + clientSecret: rest[2] as string, + }; + } + + const token = params.token; + const tokenTypeHint = params.tokenTypeHint; + const clientId = params.clientId; + const clientSecret = params.clientSecret; + + if (typeof token === 'undefined') { + throw new AppwriteException('Missing required parameter: "token"'); + } + + const apiPath = '/oauth2/{project_id}/revoke'.replace( + '{project_id}', + encodeURIComponent(String(this.client.config.project)), + ); + const apiPayload: Payload = {}; + + if (typeof token !== 'undefined') { + apiPayload['token'] = token; + } + + if (typeof tokenTypeHint !== 'undefined') { + apiPayload['token_type_hint'] = tokenTypeHint; + } + + if (typeof clientId !== 'undefined') { + apiPayload['client_id'] = clientId; + } + + if (typeof clientSecret !== 'undefined') { + apiPayload['client_secret'] = clientSecret; + } + + const uri = new URL(this.client.config.endpoint + apiPath); + return this.client.call( + 'post', + uri, + { + 'content-type': 'application/json', + accept: 'application/json', + }, + apiPayload, + ); + } + + /** + * Exchange an OAuth2 authorization code, refresh token, or device code for access and refresh tokens. + * + * @param {string} params.grantType - OAuth2 grant type. Can be one of: `authorization_code`, `refresh_token`, `urn:ietf:params:oauth:grant-type:device_code`. + * @param {string} params.code - Authorization code to be exchanged for access and refresh tokens. Required for `authorization_code` grant type. + * @param {string} params.refreshToken - Refresh token to be exchanged for a new access and refresh tokens. Required for `refresh_token` grant type. + * @param {string} params.deviceCode - Device code obtained from the device authorization endpoint. Required for `urn:ietf:params:oauth:grant-type:device_code` grant type. + * @param {string} params.clientId - OAuth2 client ID. Either a registered app ID or an HTTPS client ID metadata document URL. + * @param {string} params.clientSecret - OAuth2 client secret. Required for confidential apps. + * @param {string} params.codeVerifier - PKCE code verifier. Required for public apps. + * @param {string} params.redirectUri - Redirect URI. Required for `authorization_code` grant type. + * @param {string} params.resource - RFC 8707 resource indicator URI or URI list. Each value must be an absolute URI without a fragment. + * @param {string} params.audience - Compatibility alias for a single OAuth2 resource indicator URI. + * @throws {AppwriteException} + * @returns {Promise} + */ + createToken(params: { + grantType: string; + code?: string; + refreshToken?: string; + deviceCode?: string; + clientId?: string; + clientSecret?: string; + codeVerifier?: string; + redirectUri?: string; + resource?: string; + audience?: string; + }): Promise; + /** + * Exchange an OAuth2 authorization code, refresh token, or device code for access and refresh tokens. + * + * @param {string} grantType - OAuth2 grant type. Can be one of: `authorization_code`, `refresh_token`, `urn:ietf:params:oauth:grant-type:device_code`. + * @param {string} code - Authorization code to be exchanged for access and refresh tokens. Required for `authorization_code` grant type. + * @param {string} refreshToken - Refresh token to be exchanged for a new access and refresh tokens. Required for `refresh_token` grant type. + * @param {string} deviceCode - Device code obtained from the device authorization endpoint. Required for `urn:ietf:params:oauth:grant-type:device_code` grant type. + * @param {string} clientId - OAuth2 client ID. Either a registered app ID or an HTTPS client ID metadata document URL. + * @param {string} clientSecret - OAuth2 client secret. Required for confidential apps. + * @param {string} codeVerifier - PKCE code verifier. Required for public apps. + * @param {string} redirectUri - Redirect URI. Required for `authorization_code` grant type. + * @param {string} resource - RFC 8707 resource indicator URI or URI list. Each value must be an absolute URI without a fragment. + * @param {string} audience - Compatibility alias for a single OAuth2 resource indicator URI. + * @throws {AppwriteException} + * @returns {Promise} + * @deprecated Use the object parameter style method for a better developer experience. + */ + createToken( + grantType: string, + code?: string, + refreshToken?: string, + deviceCode?: string, + clientId?: string, + clientSecret?: string, + codeVerifier?: string, + redirectUri?: string, + resource?: string, + audience?: string, + ): Promise; + createToken( + paramsOrFirst: + | { + grantType: string; + code?: string; + refreshToken?: string; + deviceCode?: string; + clientId?: string; + clientSecret?: string; + codeVerifier?: string; + redirectUri?: string; + resource?: string; + audience?: string; + } + | string, + ...rest: [ + string?, + string?, + string?, + string?, + string?, + string?, + string?, + string?, + string?, + ] + ): Promise { + let params: { + grantType: string; + code?: string; + refreshToken?: string; + deviceCode?: string; + clientId?: string; + clientSecret?: string; + codeVerifier?: string; + redirectUri?: string; + resource?: string; + audience?: string; + }; + + if ( + paramsOrFirst && + typeof paramsOrFirst === 'object' && + !Array.isArray(paramsOrFirst) + ) { + params = (paramsOrFirst || {}) as { + grantType: string; + code?: string; + refreshToken?: string; + deviceCode?: string; + clientId?: string; + clientSecret?: string; + codeVerifier?: string; + redirectUri?: string; + resource?: string; + audience?: string; + }; + } else { + params = { + grantType: paramsOrFirst as string, + code: rest[0] as string, + refreshToken: rest[1] as string, + deviceCode: rest[2] as string, + clientId: rest[3] as string, + clientSecret: rest[4] as string, + codeVerifier: rest[5] as string, + redirectUri: rest[6] as string, + resource: rest[7] as string, + audience: rest[8] as string, + }; + } + + const grantType = params.grantType; + const code = params.code; + const refreshToken = params.refreshToken; + const deviceCode = params.deviceCode; + const clientId = params.clientId; + const clientSecret = params.clientSecret; + const codeVerifier = params.codeVerifier; + const redirectUri = params.redirectUri; + const resource = params.resource; + const audience = params.audience; + + if (typeof grantType === 'undefined') { + throw new AppwriteException( + 'Missing required parameter: "grantType"', + ); + } + + const apiPath = '/oauth2/{project_id}/token'.replace( + '{project_id}', + encodeURIComponent(String(this.client.config.project)), + ); + const apiPayload: Payload = {}; + + if (typeof grantType !== 'undefined') { + apiPayload['grant_type'] = grantType; + } + + if (typeof code !== 'undefined') { + apiPayload['code'] = code; + } + + if (typeof refreshToken !== 'undefined') { + apiPayload['refresh_token'] = refreshToken; + } + + if (typeof deviceCode !== 'undefined') { + apiPayload['device_code'] = deviceCode; + } + + if (typeof clientId !== 'undefined') { + apiPayload['client_id'] = clientId; + } + + if (typeof clientSecret !== 'undefined') { + apiPayload['client_secret'] = clientSecret; + } + + if (typeof codeVerifier !== 'undefined') { + apiPayload['code_verifier'] = codeVerifier; + } + + if (typeof redirectUri !== 'undefined') { + apiPayload['redirect_uri'] = redirectUri; + } + + if (typeof resource !== 'undefined') { + apiPayload['resource'] = resource; + } + + if (typeof audience !== 'undefined') { + apiPayload['audience'] = audience; + } + + const uri = new URL(this.client.config.endpoint + apiPath); + return this.client.call( + 'post', + uri, + { + 'content-type': 'application/json', + accept: 'application/json', + }, + apiPayload, + ); + } +} diff --git a/src/services/organization.ts b/src/services/organization.ts index 66804519..3542965f 100644 --- a/src/services/organization.ts +++ b/src/services/organization.ts @@ -60,14 +60,14 @@ export class Organization extends Service { const total = params.total; const apiPath = '/organization/installations'; - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof queries !== 'undefined') { - payload['queries'] = queries; + apiPayload['queries'] = queries; } if (typeof total !== 'undefined') { - payload['total'] = total; + apiPayload['total'] = total; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -78,7 +78,7 @@ export class Organization extends Service { 'X-Appwrite-Project': this.client.config.project, accept: 'application/json', }, - payload, + apiPayload, ); } @@ -138,14 +138,14 @@ export class Organization extends Service { } const apiPath = '/organization/installations'; - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof appId !== 'undefined') { - payload['appId'] = appId; + apiPayload['appId'] = appId; } if (typeof authorizationDetails !== 'undefined') { - payload['authorizationDetails'] = authorizationDetails; + apiPayload['authorizationDetails'] = authorizationDetails; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -157,7 +157,7 @@ export class Organization extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -209,7 +209,7 @@ export class Organization extends Service { '{installationId}', encodeURIComponent(String(installationId)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; const uri = new URL(this.client.config.endpoint + apiPath); return this.client.call( @@ -219,7 +219,7 @@ export class Organization extends Service { 'X-Appwrite-Project': this.client.config.project, accept: 'application/json', }, - payload, + apiPayload, ); } @@ -284,10 +284,10 @@ export class Organization extends Service { '{installationId}', encodeURIComponent(String(installationId)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof authorizationDetails !== 'undefined') { - payload['authorizationDetails'] = authorizationDetails; + apiPayload['authorizationDetails'] = authorizationDetails; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -299,7 +299,7 @@ export class Organization extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -349,7 +349,7 @@ export class Organization extends Service { '{installationId}', encodeURIComponent(String(installationId)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; const uri = new URL(this.client.config.endpoint + apiPath); return this.client.call( @@ -360,7 +360,7 @@ export class Organization extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } } diff --git a/src/services/presences.ts b/src/services/presences.ts index b45515a2..5fb3a364 100644 --- a/src/services/presences.ts +++ b/src/services/presences.ts @@ -70,18 +70,18 @@ export class Presences extends Service { const ttl = params.ttl; const apiPath = '/presences'; - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof queries !== 'undefined') { - payload['queries'] = queries; + apiPayload['queries'] = queries; } if (typeof total !== 'undefined') { - payload['total'] = total; + apiPayload['total'] = total; } if (typeof ttl !== 'undefined') { - payload['ttl'] = ttl; + apiPayload['ttl'] = ttl; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -92,7 +92,7 @@ export class Presences extends Service { 'X-Appwrite-Project': this.client.config.project, accept: 'application/json', }, - payload, + apiPayload, ); } @@ -144,7 +144,7 @@ export class Presences extends Service { '{presenceId}', encodeURIComponent(String(presenceId)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; const uri = new URL(this.client.config.endpoint + apiPath); return this.client.call( @@ -154,7 +154,7 @@ export class Presences extends Service { 'X-Appwrite-Project': this.client.config.project, accept: 'application/json', }, - payload, + apiPayload, ); } @@ -259,22 +259,22 @@ export class Presences extends Service { '{presenceId}', encodeURIComponent(String(presenceId)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof status !== 'undefined') { - payload['status'] = status; + apiPayload['status'] = status; } if (typeof permissions !== 'undefined') { - payload['permissions'] = permissions; + apiPayload['permissions'] = permissions; } if (typeof expiresAt !== 'undefined') { - payload['expiresAt'] = expiresAt; + apiPayload['expiresAt'] = expiresAt; } if (typeof metadata !== 'undefined') { - payload['metadata'] = metadata; + apiPayload['metadata'] = metadata; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -286,7 +286,7 @@ export class Presences extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -396,26 +396,26 @@ export class Presences extends Service { '{presenceId}', encodeURIComponent(String(presenceId)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof status !== 'undefined') { - payload['status'] = status; + apiPayload['status'] = status; } if (typeof expiresAt !== 'undefined') { - payload['expiresAt'] = expiresAt; + apiPayload['expiresAt'] = expiresAt; } if (typeof metadata !== 'undefined') { - payload['metadata'] = metadata; + apiPayload['metadata'] = metadata; } if (typeof permissions !== 'undefined') { - payload['permissions'] = permissions; + apiPayload['permissions'] = permissions; } if (typeof purge !== 'undefined') { - payload['purge'] = purge; + apiPayload['purge'] = purge; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -427,7 +427,7 @@ export class Presences extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -477,7 +477,7 @@ export class Presences extends Service { '{presenceId}', encodeURIComponent(String(presenceId)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; const uri = new URL(this.client.config.endpoint + apiPath); return this.client.call( @@ -488,7 +488,7 @@ export class Presences extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } } diff --git a/src/services/push.ts b/src/services/push.ts new file mode 100644 index 00000000..c2eb8879 --- /dev/null +++ b/src/services/push.ts @@ -0,0 +1,1531 @@ +import '../lib/polyfills'; + +import { Buffer } from 'buffer'; +// The RN/Metro build of `mqtt` (dist/mqtt.esm.js, picked via the package's +// "react-native" export condition) exposes ONLY a default export — there is no +// named `MqttClient` at runtime, so importing it as a value yields undefined +// ("undefined cannot be used as a constructor"). Import the default and read the +// class off it; keep MqttClient as a type-only import (erased at runtime). +import mqtt, { + type IClientOptions, + type IConnackPacket, + type IPublishPacket, + type MqttClient, +} from 'mqtt'; +import { NativeEventEmitter, NativeModules, Platform } from 'react-native'; + +import { Client } from '../client'; +import { createTcpStream } from '../lib/tcp-stream'; +import { Service } from '../service'; +import type { ResolvedTopic, Topic } from '../topic'; + +/** A message delivered on a subscribed topic (the MQTT analog of a Realtime event). */ +export interface PushMessage { + topic: string; + /** The payload decoded as UTF-8 text, as it was sent. */ + data: string; + /** The raw payload, for binary messages. */ + payload: Buffer; + qos: number; +} + +export type MessageCallback = (message: PushMessage) => void | Promise; + +/** Per-subscription options passed to `subscribe` and `PushSubscription.update`. */ +export interface SubscribeOptions { + /** + * Keep receiving this subscription's messages while the app is backgrounded or closed, + * posting a local notification per message. Defaults to false. + * + * On Android the SDK's native module (autolinked; it does not run in Expo Go) saves the + * subscription and keeps delivering after the app is killed, the device restarts or the app + * updates, until it is unsubscribed or {@link Push.close} is called: a scheduled job and + * alarm wake the app every 15 to 60 seconds to reconnect, and the broker replays what was + * sent in between (`retry`). {@link Push.setForeground} adds a foreground service for + * immediate delivery. It reconnects with the credential saved at subscribe time, so use a + * session rather than a short-lived JWT. + * + * Elsewhere it runs the connection in a background task via + * `react-native-background-actions` and posts notifications via `expo-notifications`, + * both peer dependencies. + */ + background?: boolean; + /** Notification title for this subscription's messages. Defaults to the message topic. */ + title?: string; + /** + * Retry delivery of messages missed while disconnected. `true` (the default) subscribes + * at QoS 1 so the broker holds this topic's messages and redelivers them on reconnect; + * `false` uses QoS 0 (at-most-once, may be lost). Per-subscription — the connection + * always keeps its session (clean start off). + */ + retry?: boolean; +} + +/** + * Handle for a live subscription. `unsubscribe()` drops it (and closes the connection + * once the last one is gone); `update()` toggles this subscription's background/title + * without resubscribing. + */ +export interface PushSubscription { + unsubscribe(): void; + update(options: SubscribeOptions): void; +} + +type AuthMethod = 'appwrite-jwt' | 'appwrite-session' | ''; + +// Fixed connection tuning — not exposed as options. +const KEEP_ALIVE_SECONDS = 60; +const RECONNECT_PERIOD_MS = 2000; + +/** + * Appwrite native push service — the Realtime analog over an MQTT broker, without + * FCM or APNS. While the app is in the foreground the connection stays open and each + * message is handed to your callback; a subscription can also opt into background + * delivery (Android foreground service + local notification per message). + * + * The connection has no tunables — it always keeps its session (clean start off) so the + * broker can redeliver missed messages, and reliability is chosen per subscription via + * `retry`. TLS (and skipping cert verification via a `?tlsInsecure=true` query + * flag) is derived from the endpoint URL, so the whole connection is described in one + * place. `subscribe` opens the connection lazily and returns a {@link PushSubscription} + * handle (unlike Realtime's bare + * unsubscribe callable — Push carries per-subscription state). The credential is read off + * the client — set a JWT or session on it (`Client.setJWT` / `Client.setSession`), the + * same way every other service reads auth. + * + * const client = new Client().setEndpoint(...).setProject(...).setJWT(jwt); + * const push = new Push(client); + * const sub = await push.subscribe('user/123/#', (m) => + * console.log(m.topic, m.data), + * ); + * // sub.update({ background: true }); sub.unsubscribe(); + */ +export class Push extends Service { + private readonly host: string; + private readonly port: number; + private readonly tls: boolean; + private readonly tlsInsecure: boolean; + + private mqtt: MqttClient | null = null; + private connecting: Promise | null = null; + /** Set once the first CONNACK lands, so 'connect' events after are reconnects. */ + private everConnected = false; + + /** Whether the background task (outside Android) is currently running. */ + private backgroundActive = false; + + /** The native Android module that hosts background delivery; null elsewhere. */ + private readonly native: NativePush | null = nativePush(); + /** Whether the subscriptions currently live on the native module's connection. */ + private nativeActive = false; + /** Bumped when the subscriptions move to the native host, superseding an open() in flight. */ + private connectionEpoch = 0; + /** Bumped by close(), so a subscribe that was waiting on a connection stops instead of reopening it. */ + private closeGeneration = 0; + private nativeEvents: { remove(): void }[] | null = null; + + private onOpenCb?: () => void; + private onCloseCb?: () => void; + private onErrorCb?: (error: Error) => void; + /** Errors already handed to onError, so one failure is reported once. */ + private reported = new WeakSet(); + /** mqtt.js connect errors mapped to the error handed out for them (see connectError). */ + private connectErrors = new WeakMap(); + /** True while the first connect is in flight: its errors go to the waiting caller. */ + private opening = false; + + // Local id -> subscription state. The id is never sent to the broker; it only lets + // the same topic carry more than one callback (each with its own options). + /** + * The client id when the credential names no user (and none was set): random, but kept for + * this instance so reconnects resume the same broker session. + */ + private readonly fallbackClientId = randomId(); + + private cookieSession = ''; + private connectedKey = ''; + + private readonly subscriptions = new Map< + string, + { + topic: string; + callback: MessageCallback; + background: boolean; + title?: string; + qos: 0 | 1; + } + >(); + + constructor(client: Client) { + super(client); + + // The broker location comes from Client.setPushEndpoint() when set (e.g. + // "mqtts://host:8883"), otherwise from the regular endpoint. + let endpointHost = ''; + let endpointSecure = false; + let endpointPort: number | null = null; + let endpointInsecure = false; + try { + const url = new URL( + client.config.endpointPush || client.config.endpoint || '', + ); + endpointHost = url.hostname; + endpointSecure = ['https:', 'wss:', 'mqtts:', 'ssl:'].includes( + url.protocol, + ); + // Take the port from the push endpoint only (a regular http/https endpoint's + // port is the API port, not the broker's). + endpointPort = + client.config.endpointPush && url.port + ? Number(url.port) + : null; + // Skip TLS cert verification (self-signed brokers / testing) via a query flag + // on the push endpoint, e.g. "mqtts://host:8883?tlsInsecure=true" — so the + // whole connection is described by one URL, not a second config knob. + const insecure = url.searchParams.get('tlsInsecure'); + endpointInsecure = insecure === 'true' || insecure === '1'; + } catch { + // leave defaults + } + + // When no explicit push endpoint is set, default to a `push.` host on the regular + // endpoint (mirroring how realtime derives from the endpoint, on the push subdomain). + if (endpointHost && !client.config.endpointPush) { + endpointHost = `push.${endpointHost}`; + } + this.host = endpointHost || 'localhost'; + // Secure by default: TLS on when the endpoint scheme is secure, so the credential + // in the CONNECT packet is not sent over a plaintext socket. + this.tls = endpointSecure; + this.port = endpointPort ?? (this.tls ? 8883 : 1883); + this.tlsInsecure = endpointInsecure; + + // Resume background delivery saved by an earlier run now, instead of at its next + // scheduled wake-up. + this.native?.resume().catch((err) => this.report(err)); + } + + // The broker subscription for a filter uses the highest QoS any local subscription on + // it wants, so a QoS-0 subscription never downgrades a QoS-1 one sharing the filter. + private effectiveQos(filter: string): 0 | 1 { + for (const sub of this.subscriptions.values()) { + if (sub.topic === filter && sub.qos === 1) { + return 1; + } + } + return 0; + } + + /** Register a callback invoked when the connection opens (CONNACK success). */ + onOpen(callback: () => void): this { + this.onOpenCb = callback; + return this; + } + + /** Register a callback invoked when the connection closes. */ + onClose(callback: () => void): this { + this.onCloseCb = callback; + return this; + } + + /** Register a callback invoked on a connection error. */ + onError(callback: (error: Error) => void): this { + this.onErrorCb = callback; + // On Android, a refused credential that stopped background delivery while no callback + // was registered is delivered now. + this.native + ?.setErrorCallback(true) + .then((stopped) => { + if (stopped) { + this.report(new Error(stopped)); + } + }) + .catch((err) => this.report(err)); + return this; + } + + /** + * Android: run background delivery in a foreground service (with a quiet ongoing + * notification), for immediate delivery even after the app is killed and during Doze, + * instead of only the scheduled wake-ups. Saved, so it applies after restarts too. Call it + * while the app is in the foreground: Android 12+ refuses to start the service from the + * background. It only affects `background: true` subscriptions. A no-op elsewhere. + */ + async setForeground(enabled: boolean): Promise { + await this.native?.setForeground(enabled); + } + + /** + * The signed-in user's own topic, `users/`, for a topic-less subscribe. The id comes + * from the credential the connection authenticates with: the JWT when one is set (never + * the session, which could belong to a different user), else the session. + */ + private userTopic(): string { + const { jwt, session } = this.client.config; + const userId = jwt + ? userIdFromJwt(jwt) + : userIdFromSession(session || this.cookieSession); + if (!userId) { + throw new Error( + 'subscribe() without a topic needs a signed-in user: set a JWT or session on the client', + ); + } + return `users/${userId}`; + } + + /** + * Hand an error to the onError callback, once per error. These errors happen outside any + * call (reconnects, broker DISCONNECTs, background re-subscribes), so they are never + * thrown, which would crash the app from inside mqtt.js: with no callback registered + * they are logged, like Realtime does. + */ + private report(error: unknown): void { + if (error instanceof SupersededError) { + // Internal: a connection superseded by the native host, not a failure. + return; + } + const err = toError(error); + if (this.reported.has(err)) { + return; + } + this.reported.add(err); + if (!this.onErrorCb) { + console.error('Appwrite Push error:', err); + return; + } + try { + this.onErrorCb(err); + } catch (callbackError) { + console.error('Appwrite Push onError threw:', callbackError); + } + } + + /** + * For a call that fails to its caller: report the error to onError when a callback is + * registered, then throw it to the caller, who can catch it. + */ + private fail(error: unknown): never { + const err = toError(error); + if (this.onErrorCb) { + this.report(err); + } + throw err; + } + + // --- realtime-like API --- + + /** + * Subscribe to one or more topics. Resolves to a {@link PushSubscription} handle. + * + * Each topic is a string or a `Topic` builder, e.g. + * `Topic.path(['user', userId]).all()`. + * + * Called with only a callback, `subscribe((message) => {...}, options)` subscribes to the + * signed-in user's own topic, `users/`, like FCM/APNs delivery to a device. The + * user id is read from the JWT (or session) set on the client. This form defaults to + * `background: true` and `retry: true` (QoS 1); pass either in the options to override. + * The default falls back to foreground delivery when background delivery is unavailable + * (no native module on Android, e.g. Expo Go; the peer dependencies elsewhere); an explicit + * `background: true` still requires it, and any other background startup failure is thrown. + * + * Awaiting resolves only once the broker has acked every SUBSCRIBE (SUBACK): + * the broker only starts routing a topic once its subscription is registered, + * so publishing before the ack races the message ahead of the subscription and + * it is dropped (no retained replay on MQTT). Awaiting here makes + * subscribe-then-publish reliable. + * + * Pass `{ background: true, title }` to also keep receiving while backgrounded and + * post a notification per matching message, and `{ retry: false }` for + * at-most-once (QoS 0) delivery; toggle either later with the handle's `update`. + */ + subscribe( + callback: MessageCallback, + options?: SubscribeOptions, + ): Promise; + subscribe( + topics: + string | Topic | ResolvedTopic | (string | Topic | ResolvedTopic)[], + callback: MessageCallback, + options?: SubscribeOptions, + ): Promise; + async subscribe( + topicsOrCallback: + | string + | Topic + | ResolvedTopic + | (string | Topic | ResolvedTopic)[] + | MessageCallback, + callbackOrOptions?: MessageCallback | SubscribeOptions, + topicOptions: SubscribeOptions = {}, + ): Promise { + // Every failure reaches onError (when registered) and is thrown to the caller. + try { + return await this.subscribeWith( + topicsOrCallback, + callbackOrOptions, + topicOptions, + ); + } catch (err) { + this.fail(err); + } + } + + private async subscribeWith( + topicsOrCallback: + | string + | Topic + | ResolvedTopic + | (string | Topic | ResolvedTopic)[] + | MessageCallback, + callbackOrOptions: MessageCallback | SubscribeOptions | undefined, + topicOptions: SubscribeOptions, + ): Promise { + const generation = this.closeGeneration; + await this.loadCookieSession(); + this.throwIfClosedSince(generation); + // Topic-less form: subscribe(callback, options) targets the signed-in user's topic. + const userForm = typeof topicsOrCallback === 'function'; + const topics = userForm ? this.userTopic() : topicsOrCallback; + const callback = ( + userForm ? topicsOrCallback : callbackOrOptions + ) as MessageCallback; + const options = ( + userForm ? (callbackOrOptions ?? {}) : topicOptions + ) as SubscribeOptions; + const topicList = (Array.isArray(topics) ? topics : [topics]).map( + (topic) => topic.toString(), + ); + // The user topic defaults to background delivery, like FCM/APNs; topics default off. + let background = options.background ?? userForm; + const qos: 0 | 1 = (options.retry ?? true) ? 1 : 0; + livePushes.add(this); + + // Only an explicit `background: true` requires background delivery: when background is + // just the user topic's default and it is unavailable, deliver in the foreground + // instead. Any other background startup failure still throws. + if ( + background && + options.background === undefined && + !this.backgroundAvailable() + ) { + background = false; + } + + if (background && Platform.OS === 'android' && !this.native) { + throw new Error( + "background delivery needs the SDK's native Android module: rebuild the app " + + '(native modules do not run in Expo Go)', + ); + } + + // Android: the native module hosts the connection while any subscription, live or saved + // by an earlier run, wants background. Two connections with the same client id would + // take over each other's broker session, so every subscription, of every Push, moves + // there. + if ( + this.native && + (background || + nativeHosts.size > 0 || + (await this.native.hasSaved())) + ) { + this.throwIfClosedSince(generation); + const ids = topicList.map((topic) => { + const id = randomId(); + this.subscriptions.set(id, { + topic, + callback, + background, + title: options.title, + qos, + }); + return id; + }); + try { + await this.hostNative(); + } catch (err) { + for (const id of ids) { + this.subscriptions.delete(id); + } + await this.reconcileNative(); + throw err; + } + return this.handle(ids); + } + + // Start the background task before connecting so a missing peer dependency surfaces a + // clear error up front, matching the connection failure path. + if (background) { + await this.startBackground(); + } + this.throwIfClosedSince(generation); + + let client: MqttClient; + try { + client = await this.connect(); + } catch (err) { + // Another Push moved this one's subscriptions to the native host meanwhile: this + // subscribe goes there too. + if (err instanceof SupersededError) { + this.throwIfClosedSince(generation); + return this.subscribeWith( + topicsOrCallback, + callbackOrOptions, + topicOptions, + ); + } + throw err; + } + const epoch = this.connectionEpoch; + + const ids: string[] = []; + const acks: Promise[] = []; + for (const topic of topicList) { + const id = randomId(); + ids.push(id); + this.subscriptions.set(id, { + topic, + callback, + background, + title: options.title, + qos, + }); + acks.push( + new Promise((resolve, reject) => { + client.subscribe( + topic, + { qos: this.effectiveQos(topic) }, + (err) => (err ? reject(err) : resolve()), + ); + }), + ); + } + + // If any topic's subscribe fails, roll this call's registrations back (and drop + // any now-unused broker filter) so a failed subscribe leaks no callback, then rethrow. + try { + await Promise.all(acks); + } catch (err) { + // Another Push moved this one's subscriptions to the native host while it waited for + // SUBACK, which closed the connection: this subscribe continues there. + if (this.nativeActive) { + try { + await this.hostNative(); + } catch (hostErr) { + for (const id of ids) { + this.subscriptions.delete(id); + } + await this.reconcileNative(); + throw hostErr; + } + return this.handle(ids); + } + if (epoch !== this.connectionEpoch) { + for (const id of ids) { + this.subscriptions.delete(id); + } + this.throwIfClosedSince(generation); + return this.subscribeWith( + topicsOrCallback, + callbackOrOptions, + topicOptions, + ); + } + this.unsubscribeInProcess(ids); + throw err; + } + + return this.handle(ids); + } + + /** + * The handle for a subscribe call's subscriptions. Each call goes to wherever they live now: + * they move to the native module (Android) when a background subscription is added later, + * and back when none is left. + */ + private handle(ids: string[]): PushSubscription { + return { + unsubscribe: () => + this.nativeActive + ? this.unsubscribeNative(ids) + : this.unsubscribeInProcess(ids), + update: (next: SubscribeOptions) => + this.nativeActive + ? this.updateNative(ids, next) + : this.updateInProcess(ids, next), + }; + } + + private unsubscribeInProcess(ids: string[]): void { + // Filters whose removed sub was QoS 1: their effective QoS may now drop. + const maybeDowngrade = new Set(); + for (const id of ids) { + const entry = this.subscriptions.get(id); + this.subscriptions.delete(id); + if (entry?.qos === 1) { + maybeDowngrade.add(entry.topic); + } + // Only unsubscribe the broker filter once its last local callback is gone. + const stillUsed = [...this.subscriptions.values()].some( + (s) => s.topic === entry?.topic, + ); + const connection = this.mqtt; + if (entry && connection && !stillUsed) { + connection.unsubscribe(entry.topic, (err) => { + // Closing the connection (the last subscription is gone) fails its + // in-flight requests on purpose; that is not an error to report. + if (err && this.mqtt === connection) { + this.report(err); + } + }); + } + } + // A filter that lost its last QoS-1 sub but still has QoS-0 subs must be + // re-SUBSCRIBEd at the lower QoS, or the broker keeps replaying to at-most-once subs. + for (const filter of maybeDowngrade) { + const stillUsed = [...this.subscriptions.values()].some( + (s) => s.topic === filter, + ); + if (stillUsed && this.effectiveQos(filter) === 0) { + this.mqtt?.subscribe(filter, { qos: 0 }, (err) => { + if (err) { + this.report(err); + } + }); + } + } + if (this.subscriptions.size === 0) { + this.teardown(); + } else { + // A removed sub may have been the last background one. + this.syncBackground(); + } + } + + private updateInProcess(ids: string[], next: SubscribeOptions): void { + const changedFilters = new Set(); + // Remember the pre-update QoS per id so a rejected re-SUBSCRIBE can be rolled back. + const previous = new Map(); + for (const id of ids) { + const entry = this.subscriptions.get(id); + if (!entry) { + continue; + } + if (next.background !== undefined) { + entry.background = next.background; + } + if (next.title !== undefined) { + entry.title = next.title; + } + if (next.retry !== undefined) { + const q: 0 | 1 = next.retry ? 1 : 0; + if (q !== entry.qos) { + previous.set(id, entry.qos); + entry.qos = q; + changedFilters.add(entry.topic); + } + } + } + // Re-SUBSCRIBE the affected filters at their new effective QoS (a SUBSCRIBE to an + // existing filter just updates its QoS — no unsubscribe gap). If the broker rejects + // it, the old subscription still stands, so restore the local QoS to match and + // surface the error rather than silently assuming the change took effect. + for (const filter of changedFilters) { + this.mqtt?.subscribe( + filter, + { qos: this.effectiveQos(filter) }, + (err) => { + if (!err) { + return; + } + for (const [id, q] of previous) { + const entry = this.subscriptions.get(id); + if (entry && entry.topic === filter) { + entry.qos = q; + } + } + this.report(err); + }, + ); + } + // Turning background on moves the subscriptions to the native module on Android. + this.syncBackground(); + } + + private unsubscribeNative(ids: string[]): void { + for (const id of ids) { + this.subscriptions.delete(id); + } + this.reconcileNative().catch((err) => this.report(err)); + } + + private updateNative(ids: string[], next: SubscribeOptions): void { + for (const id of ids) { + const entry = this.subscriptions.get(id); + if (!entry) { + continue; + } + if (next.background !== undefined) { + entry.background = next.background; + } + if (next.title !== undefined) { + entry.title = next.title; + } + if (next.retry !== undefined) { + entry.qos = next.retry ? 1 : 0; + } + } + this.reconcileNative().catch((err) => this.report(err)); + } + + /** After an in-process change: the native module on Android, the background task elsewhere. */ + private syncBackground(): void { + if (this.native) { + this.reconcileNative().catch((err) => this.report(err)); + } else { + this.syncBackgroundService(); + } + } + + /** Whether background delivery can run: the native module on Android, the peers elsewhere. */ + private backgroundAvailable(): boolean { + return Platform.OS === 'android' + ? this.native !== null + : backgroundPeersInstalled(); + } + + /** + * Host every subscription of every Push on the native module's connection, closing this + * one's in-process connection. Resolves once the connection is up and every filter is + * subscribed (SUBACK), or rejects with why not. + */ + private hostNative(): Promise { + const native = this.native!; + const key = this.credentialKey(); + // The native host carries one connection, so it serves one credential: instances hosted + // for another move back to their own connection. Instances with this credential share + // its client id, so their in-process connections would take over the broker session: + // they move to the native host too. + for (const push of nativeHosts) { + if (push.credentialKey() !== key) { + push.leaveToInProcess(); + if (push.hasBackgroundSubs()) { + push.report(new Error(BACKGROUND_DISPLACED)); + } + } + } + // Once moved, they stay: if hosting fails, the native host keeps their subscriptions and + // retries on its scheduled runs, and a second connection with the same client id would + // take over its broker session. + for (const push of livePushes) { + if (push.subscriptions.size > 0 && push.credentialKey() === key) { + push.joinNative(native); + } + } + this.joinNative(native); + return enqueueNative(() => this.sendToNative(native)); + } + + /** Host every native host's subscriptions with this Push's credential. */ + private async sendToNative(native: NativePush): Promise { + // Host what is subscribed when this runs, so a close() or an unsubscribe queued + // meanwhile is not undone by an older request. + const subscriptions = [...nativeHosts].flatMap((push) => + push.nativeEntries(), + ); + if (subscriptions.length === 0) { + return; + } + const { authMethod, credential } = this.credential(); + const config = { + host: this.host, + port: this.port, + tls: this.tls, + tlsInsecure: this.tlsInsecure, + clientId: this.client.config.pushClientId || '', + authMethod, + credential, + project: this.client.config.project ?? '', + }; + await native.setErrorCallback( + [...nativeHosts].some((push) => push.onErrorCb !== undefined), + ); + await native.host( + JSON.stringify(config), + JSON.stringify(subscriptions), + ); + } + + /** Move this Push's subscriptions onto the native host, closing its own connection. */ + private joinNative(native: NativePush): void { + // An open() still in flight sees this and closes what it opened. + this.connectionEpoch++; + if (this.mqtt) { + this.mqtt.end(true); + this.mqtt = null; + } + this.connecting = null; + this.everConnected = false; + this.listenNative(native); + nativeHosts.add(this); + this.nativeActive = true; + } + + /** Move this Push's subscriptions back onto its own connection. */ + private leaveToInProcess(): void { + nativeHosts.delete(this); + this.nativeActive = false; + this.connect() + .then(() => this.resubscribeAll()) + .catch((err) => this.report(err)); + } + + /** Identifies the connection this Push authenticates: one native host serves one. */ + private credentialKey(): string { + try { + const { authMethod, credential } = this.credential(); + return [ + this.host, + this.port, + this.client.config.project ?? '', + this.client.config.pushClientId ?? '', + authMethod, + credential, + ].join('|'); + } catch { + return ''; + } + } + + /** This Push's subscriptions in the native module's format. */ + private nativeEntries(): object[] { + return [...this.subscriptions].map(([id, sub]) => ({ + id, + topic: sub.topic, + background: sub.background, + title: sub.title ?? null, + retry: sub.qos === 1, + })); + } + + /** + * After an unsubscribe or update: keep the native module hosting while any subscription, + * live (of any Push) or saved, wants background, else move the live ones back in-process. + */ + private async reconcileNative(): Promise { + const native = this.native; + if (!native) { + return; + } + if (this.subscriptions.size === 0) { + // This Push has nothing left; saved subscriptions keep delivering in the background. + if (this.nativeActive) { + await this.leaveNative(native); + } + return; + } + const wanted = + [...nativeHosts, this].some((push) => push.hasBackgroundSubs()) || + (await native.hasSaved()); + if (wanted) { + await this.hostNative(); + return; + } + // Nothing wants background: every Push moves back to its own connection. + const hosts = [...nativeHosts]; + nativeHosts.clear(); + for (const push of hosts) { + push.nativeActive = false; + } + await enqueueNative(() => native.release()); + for (const push of hosts) { + if (push.subscriptions.size > 0) { + await push.connect(); + push.resubscribeAll(); + } + } + } + + /** Stop hosting this Push natively; the others' subscriptions stay hosted. */ + private leaveNative(native: NativePush): Promise { + nativeHosts.delete(this); + this.nativeActive = false; + const [other] = nativeHosts; + return other + ? other.hostNative() + : enqueueNative(() => native.release()); + } + + /** + * Route the native module's messages to their subscriptions, and its errors to onError. A + * message is acknowledged once its callback has run, as on the in-process connection. + */ + private listenNative(native: NativePush): void { + if (this.nativeEvents) { + return; + } + const emitter = new NativeEventEmitter(native); + this.nativeEvents = [ + emitter.addListener( + 'AppwritePushMessage', + (event: NativeMessage) => { + const sub = this.subscriptions.get(event.id); + if (!sub) { + return; + } + const payload = Buffer.from(event.payload, 'base64'); + const message: PushMessage = { + topic: event.topic, + data: payload.toString('utf8'), + payload, + qos: event.qos, + }; + Promise.resolve() + .then(() => sub.callback(message)) + .catch((err) => this.report(err)) + .finally(() => native.ack(event.ackToken)); + }, + ), + emitter.addListener( + 'AppwritePushError', + (event: { message: string }) => + this.report(new Error(event.message)), + ), + ]; + } + + /** + * Tear down the connection and drop all subscriptions. On Android, closing the last Push + * that uses background delivery also stops it, including subscriptions saved by an earlier + * run: call it on sign-out. While other Push instances still use it, it keeps delivering + * their subscriptions and the saved ones. + */ + close(): void { + this.connectionEpoch++; + this.closeGeneration++; + this.teardown(); + const native = this.native; + if (!native) { + return; + } + nativeHosts.delete(this); + this.nativeActive = false; + this.nativeEvents?.forEach((subscription) => subscription.remove()); + this.nativeEvents = null; + const [other] = nativeHosts; + if (other) { + other.hostNative().catch((err) => other.report(err)); + return; + } + enqueueNative(() => native.stop()).catch((err) => this.report(err)); + } + + /** Close the in-process connection and drop the live subscriptions. */ + private teardown(): void { + livePushes.delete(this); + if (this.mqtt) { + this.mqtt.end(true); + this.mqtt = null; + } + this.connecting = null; + this.everConnected = false; + this.subscriptions.clear(); + this.stopBackground(); + } + + /** Throws once close() has run since `generation` was read, so a subscribe started before it stops. */ + private throwIfClosedSince(generation: number): void { + if (generation !== this.closeGeneration) { + throw closedError(); + } + } + + /** True while any live subscription has opted into background delivery. */ + private hasBackgroundSubs(): boolean { + for (const sub of this.subscriptions.values()) { + if (sub.background) { + return true; + } + } + return false; + } + + // Reconcile the reference-counted foreground service with whether any live + // subscription still wants background delivery. + private syncBackgroundService(): void { + const want = this.hasBackgroundSubs(); + if (want && !this.backgroundActive) { + this.startBackground().catch((err) => this.report(err)); + } else if (!want && this.backgroundActive) { + this.stopBackground(); + } + } + + /** Stop the Android foreground service if it is running (no-op otherwise). */ + private stopBackground(): void { + if (!this.backgroundActive) { + return; + } + this.backgroundActive = false; + try { + /* eslint-disable @typescript-eslint/no-require-imports */ + const backgroundActions: typeof import('react-native-background-actions').default = + require('react-native-background-actions').default; + /* eslint-enable @typescript-eslint/no-require-imports */ + backgroundActions.stop().catch((err) => this.report(err)); + } catch { + // not installed or already stopped + } + } + + /** + * Start the Android foreground service (keeps the process alive so the in-process + * MQTT socket keeps delivering) and request notification permission. No-op after + * the first call. + */ + private async startBackground(): Promise { + if (this.backgroundActive) { + return; + } + this.backgroundActive = true; + try { + /* eslint-disable @typescript-eslint/no-require-imports */ + const notifications: typeof import('expo-notifications') = require('expo-notifications'); + const backgroundActions: typeof import('react-native-background-actions').default = + require('react-native-background-actions').default; + /* eslint-enable @typescript-eslint/no-require-imports */ + await notifications.requestPermissionsAsync(); + await notifications.setNotificationChannelAsync('appwrite-push', { + name: 'Notifications', + importance: notifications.AndroidImportance.HIGH, + }); + if (!backgroundActions.isRunning()) { + await backgroundActions.start( + // The MQTT client runs in this same JS context; the task only has to + // stay pending to keep the foreground service (and process) alive. + () => new Promise(() => undefined), + { + taskName: 'appwritePush', + taskTitle: 'Appwrite', + taskDesc: 'Listening for messages', + // taskIcon is required by react-native-background-actions — without + // it start() throws "Task icon not found". ic_launcher/mipmap exists + // in every Expo/RN Android app, so it is a safe default. + taskIcon: { name: 'ic_launcher', type: 'mipmap' }, + // Match the dataSync foregroundServiceType the config plugin injects + // into the manifest, so the service is valid on Android 14+. + foregroundServiceType: ['dataSync'], + }, + ); + } + } catch (err) { + this.backgroundActive = false; + // Surface the real failure (e.g. a missing taskIcon, or a missing peer module) + // instead of assuming it is always a missing dependency. + const detail = err instanceof Error ? err.message : String(err); + throw new Error( + `background delivery failed to start: ${detail} (it needs the ` + + '"react-native-background-actions" and "expo-notifications" peer ' + + 'dependencies)', + { cause: err }, + ); + } + } + + /** Post a local notification for a message a background subscription matched. */ + private async notify(message: PushMessage, title?: string): Promise { + try { + /* eslint-disable @typescript-eslint/no-require-imports */ + const notifications: typeof import('expo-notifications') = require('expo-notifications'); + /* eslint-enable @typescript-eslint/no-require-imports */ + await notifications.scheduleNotificationAsync({ + content: { + title: title ?? message.topic, + body: message.data, + }, + trigger: null, + }); + } catch { + // expo-notifications not installed; skip. + } + } + + /** + * Re-register every active subscription. Called on each reconnect: with + * a fresh (or cleared) session the broker keeps no subscriptions, so a reconnected client starts with + * zero subscriptions and would otherwise silently stop receiving. + */ + private resubscribeAll(): void { + const client = this.mqtt; + if (!client) { + return; + } + const filters = new Set( + [...this.subscriptions.values()].map((s) => s.topic), + ); + for (const filter of filters) { + client.subscribe( + filter, + { qos: this.effectiveQos(filter) }, + (err) => { + if (err) { + this.report(err); + } + }, + ); + } + } + + // --- internals --- + + private connect(): Promise { + if ( + (this.mqtt || this.connecting) && + this.connectedKey !== this.credentialKey() + ) { + this.connectionEpoch++; + this.mqtt?.end(true); + this.mqtt = null; + this.connecting = null; + this.everConnected = this.subscriptions.size > 0; + } + if (this.mqtt && !this.connecting) { + return Promise.resolve(this.mqtt); + } + if (!this.connecting) { + const connecting = this.open(); + this.connecting = connecting; + // A failed open must not poison future attempts — clear the cached + // in-flight promise so a later subscribe/publish can retry. + connecting.catch(() => { + if (this.connecting === connecting) { + this.connecting = null; + } + }); + } + return this.connecting.then(() => this.mqtt!); + } + + private async loadCookieSession(): Promise { + const { jwt, session, endpoint, project } = this.client.config; + if (jwt || session) { + return; + } + this.cookieSession = await sessionCookie(endpoint, project); + } + + /** The credential set on the client (via Client.setJWT / setSession). */ + private credential(): { authMethod: AuthMethod; credential: string } { + if (this.client.config.jwt) { + return { + authMethod: 'appwrite-jwt', + credential: this.client.config.jwt, + }; + } + const session = this.client.config.session || this.cookieSession; + if (session) { + return { + authMethod: 'appwrite-session', + credential: session, + }; + } + throw new Error( + 'No credential set on the client; call Client.setJWT() or Client.setSession() first.', + ); + } + + private async open(): Promise { + const { authMethod, credential } = this.credential(); + this.connectedKey = this.credentialKey(); + const epoch = this.connectionEpoch; + + const project = this.client.config.project ?? ''; + // Client id, when the app did not set one with Client.setPushClientId(). Unlike the + // native SDKs (which send an empty id and let the broker derive a stable one), mqtt.js + // rejects an empty id with clean start off, so derive a stable per-user id here from the + // credential this connection authenticates with: the JWT's userId, or the session's user + // id. A stable id keeps the broker's replay cursor resuming across restarts. The + // credential itself is never used as the id. + // On Android the native module's per-user-and-install id is used, so this connection + // and the background one share the broker session. + const clientId = + this.client.config.pushClientId || + (this.native + ? await this.native.defaultClientId(authMethod, credential) + : (authMethod === 'appwrite-jwt' + ? userIdFromJwt(credential) + : userIdFromSession(credential)) || + this.fallbackClientId); + // The subscriptions moved to the native host while this was waiting: its connection + // would share the host's client id and take over the broker session. + if (epoch !== this.connectionEpoch) { + throw new SupersededError(); + } + + const options: IClientOptions = { + clientId, + protocolVersion: 5, + clean: false, // always keep the session so the broker can replay QoS-1 topics + keepalive: KEEP_ALIVE_SECONDS, + reconnectPeriod: RECONNECT_PERIOD_MS, + manualConnect: true, + // Enhanced auth carried in CONNECT properties. + properties: { + authenticationMethod: authMethod, + authenticationData: Buffer.from(credential), + userProperties: { projectId: project }, + }, + // Ack QoS 1 deliveries by hand, after the callback has processed the + // message. On the broker a subscriber's PUBACK is what advances its replay + // cursor, so acking automatically on arrival — before the callback runs — + // would advance past a message a failing callback never actually handled. + customHandleAcks: (_topic, _message, packet, cb) => { + this.dispatch(packet as IPublishPacket) + // cb(0): send the PUBACK now that the callback has processed it. + .then(() => cb(0)) + // On failure, do not ack: the message stays unacked and the broker + // replays it on reconnect (at-least-once). + .catch((err) => { + this.report(err); + cb(toError(err)); + }); + }, + }; + + // Raw TCP only: build the transport from a native socket, never a WebSocket. + const client = new mqtt.MqttClient( + () => + createTcpStream({ + host: this.host, + port: this.port, + tls: this.tls, + tlsInsecure: this.tlsInsecure, + }), + options, + ); + this.mqtt = client; + + // Dispatch QoS 0 deliveries (which never reach customHandleAcks). QoS 1/2 are + // dispatched from customHandleAcks; ignore them here to avoid a double dispatch. + client.on('message', (_topic, _payload, packet) => { + if (packet.qos === 0) { + this.dispatch(packet).catch((err) => this.report(err)); + } + }); + + // Persistent lifecycle listeners. On a reconnect the broker has no session for + // us on a cleared session, so we must re-register every subscription. + client.on('connect', () => { + if (this.everConnected) { + this.resubscribeAll(); + } + this.everConnected = true; + this.onOpenCb?.(); + }); + client.on('close', () => this.onCloseCb?.()); + // The broker explains a refused CONNECT (CONNACK) or a server-initiated DISCONNECT + // with an MQTT 5 Reason String. mqtt.js reports only the reason code, so capture the + // string off the packet (packetreceive fires before mqtt.js handles it) and hand it + // to onError as the error message. + let refusedReason: string | undefined; + client.on('packetreceive', (packet) => { + if (packet.cmd === 'connack') { + refusedReason = + (packet.reasonCode ?? 0) !== 0 + ? packet.properties?.reasonString + : undefined; + } + }); + // A persistent error listener also prevents mqtt.js from throwing on + // post-connect errors once the one-shot handler below is removed. + client.on('error', (err) => { + if (epoch !== this.connectionEpoch) { + return; + } + const error = this.connectError(err, refusedReason); + // While the first connect is pending its caller receives the error (and reports it + // through fail()); afterwards, e.g. on reconnect, report it here. + if (!this.opening) { + this.report(error); + } + }); + client.on('disconnect', (packet) => { + if ((packet.reasonCode ?? 0) !== 0) { + this.report( + new Error( + packet.properties?.reasonString ?? + `Disconnected by the broker (reason ${packet.reasonCode})`, + ), + ); + } + }); + + this.opening = true; + await new Promise((resolve, reject) => { + const onConnect = (packet: IConnackPacket) => { + cleanup(); + if (epoch !== this.connectionEpoch) { + reject(new SupersededError()); + return; + } + // reasonCode (v5) / returnCode (v3) of 0 means accepted. + const rc = packet.reasonCode ?? packet.returnCode ?? 0; + if (rc !== 0) { + this.teardown(); + reject( + new Error( + refusedReason ?? + `MQTT authentication failed (CONNACK reason ${rc})`, + ), + ); + } else { + resolve(); + } + }; + const onError = (err: Error) => { + cleanup(); + if (epoch !== this.connectionEpoch) { + reject(new SupersededError()); + return; + } + this.teardown(); + reject(this.connectError(err, refusedReason)); + }; + // The subscriptions moved to the native host, which ended this client before CONNACK. + const onClose = () => { + if (epoch !== this.connectionEpoch) { + cleanup(); + reject(new SupersededError()); + } + }; + const cleanup = () => { + if (epoch === this.connectionEpoch) { + this.opening = false; + } + client.removeListener('connect', onConnect); + client.removeListener('error', onError); + client.removeListener('close', onClose); + }; + client.on('connect', onConnect); + client.on('error', onError); + client.on('close', onClose); + client.connect(); + }); + } + + /** + * The error for a failed connect: the broker's Reason String when it sent one (a refused + * CONNACK), else mqtt.js's error. Memoized so both error listeners hand out the same + * object and it is reported once. + */ + private connectError(err: unknown, reason: string | undefined): Error { + const source = toError(err); + let error = this.connectErrors.get(source); + if (!error) { + error = reason ? new Error(reason) : source; + this.connectErrors.set(source, error); + } + return error; + } + + /** Fan a delivered packet out to every subscription whose filter matches. */ + private async dispatch(packet: IPublishPacket): Promise { + const payload = Buffer.isBuffer(packet.payload) + ? packet.payload + : Buffer.from(packet.payload); + const message: PushMessage = { + topic: packet.topic.toString(), + data: payload.toString('utf8'), + payload, + qos: packet.qos, + }; + for (const sub of this.subscriptions.values()) { + if (matches(sub.topic, message.topic)) { + await sub.callback(message); + // Notification is per-subscription: only subs that opted in post one, + // each with its own title. + if (sub.background) { + await this.notify(message, sub.title); + } + } + } + } +} + +/** The SDK's native Android module (see android/), which hosts background delivery. */ +interface NativePush { + host(config: string, subscriptions: string): Promise; + ack(token: string): void; + release(): Promise; + stop(): Promise; + setForeground(enabled: boolean): Promise; + hasSaved(): Promise; + resume(): Promise; + setErrorCallback(registered: boolean): Promise; + defaultClientId(authMethod: string, credential: string): Promise; + addListener(eventName: string): void; + removeListeners(count: number): void; +} + +/** A message the native module delivers for a hosted subscription. */ +interface NativeMessage { + id: string; + topic: string; + payload: string; + qos: number; + ackToken: string; +} + +/** + * Every Push on Android shares the one native host (one connection per client id), which hosts + * the union of their subscriptions. Native calls run one at a time in call order, so a sign-out + * is never undone by a subscribe still on its way. + */ +const nativeHosts = new Set(); +/** Every Push with live subscriptions, so the native host can take over their connections. */ +const livePushes = new Set(); +let nativeQueue: Promise = Promise.resolve(); + +function enqueueNative(op: () => Promise): Promise { + const result = nativeQueue.then(op); + nativeQueue = result.catch(() => undefined); + return result; +} + +/** The native module on Android when the app was built with it (not in Expo Go), else null. */ +function nativePush(): NativePush | null { + if (Platform.OS !== 'android') { + return null; + } + return (NativeModules?.AppwritePush as NativePush | undefined) ?? null; +} + +/** + * Reported to a Push whose background delivery stopped because a Push with another credential + * took over the native host, which carries one connection and so serves one credential. + */ +const BACKGROUND_DISPLACED = + 'Background delivery stopped: a Push with another credential started background delivery, ' + + 'and the device hosts one. These subscriptions still deliver while the app runs.'; + +/** An in-process connect superseded by the native host or by a connect with another credential. */ +function closedError(): Error { + return new Error('Push was closed before the subscription was established'); +} + +class SupersededError extends Error { + constructor() { + super('The push connection was superseded'); + } +} + +/** Normalize anything thrown or passed to an error handler into an `Error`. */ +function toError(error: unknown): Error { + return error instanceof Error ? error : new Error(String(error)); +} + +/** + * The userId claim from an Appwrite JWT, used as the default MQTT client id so the + * broker's replay cursor is stable per user. Returns '' when the token is not a decodable JWT + * (e.g. a plain string), so the caller can fall back to the session or raw credential. + */ +function userIdFromJwt(jwt?: string): string { + if (!jwt) { + return ''; + } + const parts = jwt.split('.'); + if (parts.length < 2) { + return ''; + } + try { + const json = Buffer.from( + parts[1].replace(/-/g, '+').replace(/_/g, '/'), + 'base64', + ).toString('utf-8'); + const payload = JSON.parse(json); + return typeof payload.userId === 'string' ? payload.userId : ''; + } catch { + return ''; + } +} + +/** + * Whether the optional background-delivery peers (`expo-notifications` and + * `react-native-background-actions`) are installed. + */ +function backgroundPeersInstalled(): boolean { + try { + /* eslint-disable @typescript-eslint/no-require-imports */ + require('expo-notifications'); + require('react-native-background-actions'); + /* eslint-enable @typescript-eslint/no-require-imports */ + return true; + } catch { + return false; + } +} + +interface NativeCookies { + session(url: string, project: string): Promise; +} + +async function sessionCookie( + endpoint: string | undefined, + project: string | undefined, +): Promise { + const cookies = NativeModules?.AppwriteCookies as NativeCookies | undefined; + if (!cookies || !endpoint || !project) { + return ''; + } + try { + const value = await cookies.session(endpoint, project); + return value ? decodeURIComponent(value) : ''; + } catch { + return ''; + } +} + +/** + * The user id inside a session secret, which is base64 of JSON `{"id": , "secret": ...}`. + * Returns '' when the value is not such a secret. + */ +function userIdFromSession(session?: string): string { + if (!session) { + return ''; + } + try { + const payload = JSON.parse( + Buffer.from(session, 'base64').toString('utf-8'), + ); + return typeof payload.id === 'string' ? payload.id : ''; + } catch { + return ''; + } +} + +/** A hex id, uuid4-like enough for a client-generated subId / client id suffix. */ +function randomId(): string { + let out = ''; + for (let i = 0; i < 32; i++) { + out += Math.floor(Math.random() * 16).toString(16); + } + return out; +} + +/** MQTT topic-filter match with '+' (single level) and '#' (multi level). */ +function matches(filter: string, topic: string): boolean { + const filterParts = filter.split('/'); + const topicParts = topic.split('/'); + for (let i = 0; i < filterParts.length; i++) { + const part = filterParts[i]; + if (part === '#') { + return true; + } + if (i >= topicParts.length) { + return false; + } + if (part !== '+' && part !== topicParts[i]) { + return false; + } + } + return filterParts.length === topicParts.length; +} diff --git a/src/services/storage.ts b/src/services/storage.ts index 51a93df7..fdca28a8 100644 --- a/src/services/storage.ts +++ b/src/services/storage.ts @@ -98,18 +98,18 @@ export class Storage extends Service { '{bucketId}', encodeURIComponent(String(bucketId)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof queries !== 'undefined') { - payload['queries'] = queries; + apiPayload['queries'] = queries; } if (typeof search !== 'undefined') { - payload['search'] = search; + apiPayload['search'] = search; } if (typeof total !== 'undefined') { - payload['total'] = total; + apiPayload['total'] = total; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -120,7 +120,7 @@ export class Storage extends Service { 'X-Appwrite-Project': this.client.config.project, accept: 'application/json', }, - payload, + apiPayload, ); } @@ -265,30 +265,36 @@ export class Storage extends Service { '{bucketId}', encodeURIComponent(String(bucketId)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof fileId !== 'undefined') { - payload['fileId'] = fileId; + apiPayload['fileId'] = fileId; } if (typeof file !== 'undefined') { - payload['file'] = file; + apiPayload['file'] = file; } if (typeof permissions !== 'undefined') { - payload['permissions'] = permissions; + apiPayload['permissions'] = permissions; } if (typeof folder !== 'undefined') { - payload['folder'] = folder; + apiPayload['folder'] = folder; } const uri = new URL(this.client.config.endpoint + apiPath); + const apiHeaders: { [header: string]: string } = { + 'X-Appwrite-Project': this.client.config.project, + 'content-type': 'multipart/form-data', + accept: 'application/json', + }; + const size = file.size; if (size <= Service.CHUNK_SIZE) { - payload['file'] = Service.filePart( + apiPayload['file'] = Service.filePart( file.uri, file.name, file.type, @@ -298,24 +304,9 @@ export class Storage extends Service { }), ); - return this.client.call( - 'post', - uri, - { - 'X-Appwrite-Project': this.client.config.project, - 'content-type': 'multipart/form-data', - accept: 'application/json', - }, - payload, - ); + return this.client.call('post', uri, apiHeaders, apiPayload); } - const apiHeaders: { [header: string]: string } = { - 'X-Appwrite-Project': this.client.config.project, - 'content-type': 'multipart/form-data', - accept: 'application/json', - }; - let offset = 0; let response = undefined; const providedUploadId = fileId; @@ -363,7 +354,7 @@ export class Storage extends Service { }); } - payload['file'] = Service.filePart( + apiPayload['file'] = Service.filePart( firstPath, file.name, file.type, @@ -374,7 +365,7 @@ export class Storage extends Service { 'post', uri, firstChunkHeaders, - payload, + apiPayload, ); offset = firstChunkEnd; @@ -446,7 +437,7 @@ export class Storage extends Service { }); } - const chunkPayload = { ...payload }; + const chunkPayload = { ...apiPayload }; chunkPayload['file'] = Service.filePart( chunkPath, file.name, @@ -569,7 +560,7 @@ export class Storage extends Service { const apiPath = '/storage/buckets/{bucketId}/files/{fileId}' .replace('{bucketId}', encodeURIComponent(String(bucketId))) .replace('{fileId}', encodeURIComponent(String(fileId))); - const payload: Payload = {}; + const apiPayload: Payload = {}; const uri = new URL(this.client.config.endpoint + apiPath); return this.client.call( @@ -579,7 +570,7 @@ export class Storage extends Service { 'X-Appwrite-Project': this.client.config.project, accept: 'application/json', }, - payload, + apiPayload, ); } @@ -672,14 +663,14 @@ export class Storage extends Service { const apiPath = '/storage/buckets/{bucketId}/files/{fileId}' .replace('{bucketId}', encodeURIComponent(String(bucketId))) .replace('{fileId}', encodeURIComponent(String(fileId))); - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof name !== 'undefined') { - payload['name'] = name; + apiPayload['name'] = name; } if (typeof permissions !== 'undefined') { - payload['permissions'] = permissions; + apiPayload['permissions'] = permissions; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -691,7 +682,7 @@ export class Storage extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -752,7 +743,7 @@ export class Storage extends Service { const apiPath = '/storage/buckets/{bucketId}/files/{fileId}' .replace('{bucketId}', encodeURIComponent(String(bucketId))) .replace('{fileId}', encodeURIComponent(String(fileId))); - const payload: Payload = {}; + const apiPayload: Payload = {}; const uri = new URL(this.client.config.endpoint + apiPath); return this.client.call( @@ -763,7 +754,7 @@ export class Storage extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -838,18 +829,20 @@ export class Storage extends Service { const apiPath = '/storage/buckets/{bucketId}/files/{fileId}/download' .replace('{bucketId}', encodeURIComponent(String(bucketId))) .replace('{fileId}', encodeURIComponent(String(fileId))); - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof token !== 'undefined') { - payload['token'] = token; + apiPayload['token'] = token; } const uri = new URL(this.client.config.endpoint + apiPath); - payload['project'] = this.client.config.project; + apiPayload['project'] = this.client.config.project; - payload['impersonateuserid'] = this.client.config.impersonateuserid; + apiPayload['impersonateuserid'] = this.client.config.impersonateuserid; - for (const [key, value] of Object.entries(Service.flatten(payload))) { + for (const [key, value] of Object.entries( + Service.flatten(apiPayload), + )) { uri.searchParams.append(key, value); } return this.client.call( @@ -859,7 +852,7 @@ export class Storage extends Service { 'X-Appwrite-Project': this.client.config.project, accept: '*/*', }, - payload, + apiPayload, 'arrayBuffer', ); } @@ -1057,62 +1050,64 @@ export class Storage extends Service { const apiPath = '/storage/buckets/{bucketId}/files/{fileId}/preview' .replace('{bucketId}', encodeURIComponent(String(bucketId))) .replace('{fileId}', encodeURIComponent(String(fileId))); - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof width !== 'undefined') { - payload['width'] = width; + apiPayload['width'] = width; } if (typeof height !== 'undefined') { - payload['height'] = height; + apiPayload['height'] = height; } if (typeof gravity !== 'undefined') { - payload['gravity'] = gravity; + apiPayload['gravity'] = gravity; } if (typeof quality !== 'undefined') { - payload['quality'] = quality; + apiPayload['quality'] = quality; } if (typeof borderWidth !== 'undefined') { - payload['borderWidth'] = borderWidth; + apiPayload['borderWidth'] = borderWidth; } if (typeof borderColor !== 'undefined') { - payload['borderColor'] = borderColor; + apiPayload['borderColor'] = borderColor; } if (typeof borderRadius !== 'undefined') { - payload['borderRadius'] = borderRadius; + apiPayload['borderRadius'] = borderRadius; } if (typeof opacity !== 'undefined') { - payload['opacity'] = opacity; + apiPayload['opacity'] = opacity; } if (typeof rotation !== 'undefined') { - payload['rotation'] = rotation; + apiPayload['rotation'] = rotation; } if (typeof background !== 'undefined') { - payload['background'] = background; + apiPayload['background'] = background; } if (typeof output !== 'undefined') { - payload['output'] = output; + apiPayload['output'] = output; } if (typeof token !== 'undefined') { - payload['token'] = token; + apiPayload['token'] = token; } const uri = new URL(this.client.config.endpoint + apiPath); - payload['project'] = this.client.config.project; + apiPayload['project'] = this.client.config.project; - payload['impersonateuserid'] = this.client.config.impersonateuserid; + apiPayload['impersonateuserid'] = this.client.config.impersonateuserid; - for (const [key, value] of Object.entries(Service.flatten(payload))) { + for (const [key, value] of Object.entries( + Service.flatten(apiPayload), + )) { uri.searchParams.append(key, value); } return this.client.call( @@ -1122,7 +1117,7 @@ export class Storage extends Service { 'X-Appwrite-Project': this.client.config.project, accept: 'image/*', }, - payload, + apiPayload, 'arrayBuffer', ); } @@ -1198,18 +1193,20 @@ export class Storage extends Service { const apiPath = '/storage/buckets/{bucketId}/files/{fileId}/view' .replace('{bucketId}', encodeURIComponent(String(bucketId))) .replace('{fileId}', encodeURIComponent(String(fileId))); - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof token !== 'undefined') { - payload['token'] = token; + apiPayload['token'] = token; } const uri = new URL(this.client.config.endpoint + apiPath); - payload['project'] = this.client.config.project; + apiPayload['project'] = this.client.config.project; - payload['impersonateuserid'] = this.client.config.impersonateuserid; + apiPayload['impersonateuserid'] = this.client.config.impersonateuserid; - for (const [key, value] of Object.entries(Service.flatten(payload))) { + for (const [key, value] of Object.entries( + Service.flatten(apiPayload), + )) { uri.searchParams.append(key, value); } return this.client.call( @@ -1219,7 +1216,7 @@ export class Storage extends Service { 'X-Appwrite-Project': this.client.config.project, accept: '*/*', }, - payload, + apiPayload, 'arrayBuffer', ); } @@ -1249,18 +1246,20 @@ export class Storage extends Service { const apiPath = '/storage/buckets/{bucketId}/files/{fileId}/download' .replace('{bucketId}', encodeURIComponent(String(bucketId))) .replace('{fileId}', encodeURIComponent(String(fileId))); - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof token !== 'undefined') { - payload['token'] = token; + apiPayload['token'] = token; } const uri = new URL(this.client.config.endpoint + apiPath); - payload['project'] = this.client.config.project; + apiPayload['project'] = this.client.config.project; - payload['impersonateuserid'] = this.client.config.impersonateuserid; + apiPayload['impersonateuserid'] = this.client.config.impersonateuserid; - for (const [key, value] of Object.entries(Service.flatten(payload))) { + for (const [key, value] of Object.entries( + Service.flatten(apiPayload), + )) { uri.searchParams.append(key, value); } @@ -1320,62 +1319,64 @@ export class Storage extends Service { const apiPath = '/storage/buckets/{bucketId}/files/{fileId}/preview' .replace('{bucketId}', encodeURIComponent(String(bucketId))) .replace('{fileId}', encodeURIComponent(String(fileId))); - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof width !== 'undefined') { - payload['width'] = width; + apiPayload['width'] = width; } if (typeof height !== 'undefined') { - payload['height'] = height; + apiPayload['height'] = height; } if (typeof gravity !== 'undefined') { - payload['gravity'] = gravity; + apiPayload['gravity'] = gravity; } if (typeof quality !== 'undefined') { - payload['quality'] = quality; + apiPayload['quality'] = quality; } if (typeof borderWidth !== 'undefined') { - payload['borderWidth'] = borderWidth; + apiPayload['borderWidth'] = borderWidth; } if (typeof borderColor !== 'undefined') { - payload['borderColor'] = borderColor; + apiPayload['borderColor'] = borderColor; } if (typeof borderRadius !== 'undefined') { - payload['borderRadius'] = borderRadius; + apiPayload['borderRadius'] = borderRadius; } if (typeof opacity !== 'undefined') { - payload['opacity'] = opacity; + apiPayload['opacity'] = opacity; } if (typeof rotation !== 'undefined') { - payload['rotation'] = rotation; + apiPayload['rotation'] = rotation; } if (typeof background !== 'undefined') { - payload['background'] = background; + apiPayload['background'] = background; } if (typeof output !== 'undefined') { - payload['output'] = output; + apiPayload['output'] = output; } if (typeof token !== 'undefined') { - payload['token'] = token; + apiPayload['token'] = token; } const uri = new URL(this.client.config.endpoint + apiPath); - payload['project'] = this.client.config.project; + apiPayload['project'] = this.client.config.project; - payload['impersonateuserid'] = this.client.config.impersonateuserid; + apiPayload['impersonateuserid'] = this.client.config.impersonateuserid; - for (const [key, value] of Object.entries(Service.flatten(payload))) { + for (const [key, value] of Object.entries( + Service.flatten(apiPayload), + )) { uri.searchParams.append(key, value); } @@ -1407,18 +1408,20 @@ export class Storage extends Service { const apiPath = '/storage/buckets/{bucketId}/files/{fileId}/view' .replace('{bucketId}', encodeURIComponent(String(bucketId))) .replace('{fileId}', encodeURIComponent(String(fileId))); - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof token !== 'undefined') { - payload['token'] = token; + apiPayload['token'] = token; } const uri = new URL(this.client.config.endpoint + apiPath); - payload['project'] = this.client.config.project; + apiPayload['project'] = this.client.config.project; - payload['impersonateuserid'] = this.client.config.impersonateuserid; + apiPayload['impersonateuserid'] = this.client.config.impersonateuserid; - for (const [key, value] of Object.entries(Service.flatten(payload))) { + for (const [key, value] of Object.entries( + Service.flatten(apiPayload), + )) { uri.searchParams.append(key, value); } diff --git a/src/services/tables-db.ts b/src/services/tables-db.ts index 4e73fb6d..ae5d11d1 100644 --- a/src/services/tables-db.ts +++ b/src/services/tables-db.ts @@ -12,46 +12,62 @@ export class TablesDB extends Service { * List transactions across all databases. * * @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). + * @param {boolean} params.total - When set to false, the total count returned will be 0 and will not be calculated. * @throws {AppwriteException} * @returns {Promise} */ listTransactions(params?: { queries?: string[]; + total?: boolean; }): Promise; /** * List transactions across all databases. * * @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). + * @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. */ - listTransactions(queries?: string[]): Promise; listTransactions( - paramsOrFirst?: { queries?: string[] } | string[], + queries?: string[], + total?: boolean, + ): Promise; + listTransactions( + paramsOrFirst?: { queries?: string[]; total?: boolean } | string[], + ...rest: [boolean?] ): Promise { - let params: { queries?: string[] }; + let params: { queries?: string[]; total?: boolean }; if ( - typeof paramsOrFirst === 'undefined' || + (typeof paramsOrFirst === 'undefined' && rest.length === 0) || (paramsOrFirst && typeof paramsOrFirst === 'object' && !Array.isArray(paramsOrFirst)) ) { - params = (paramsOrFirst || {}) as { queries?: string[] }; + 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 = '/tablesdb/transactions'; - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof queries !== 'undefined') { - payload['queries'] = queries; + apiPayload['queries'] = queries; + } + + if (typeof total !== 'undefined') { + apiPayload['total'] = total; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -62,7 +78,7 @@ export class TablesDB extends Service { 'X-Appwrite-Project': this.client.config.project, accept: 'application/json', }, - payload, + apiPayload, ); } @@ -104,10 +120,10 @@ export class TablesDB extends Service { const ttl = params.ttl; const apiPath = '/tablesdb/transactions'; - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof ttl !== 'undefined') { - payload['ttl'] = ttl; + apiPayload['ttl'] = ttl; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -119,7 +135,7 @@ export class TablesDB extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -171,7 +187,7 @@ export class TablesDB extends Service { '{transactionId}', encodeURIComponent(String(transactionId)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; const uri = new URL(this.client.config.endpoint + apiPath); return this.client.call( @@ -181,7 +197,7 @@ export class TablesDB extends Service { 'X-Appwrite-Project': this.client.config.project, accept: 'application/json', }, - payload, + apiPayload, ); } @@ -258,14 +274,14 @@ export class TablesDB extends Service { '{transactionId}', encodeURIComponent(String(transactionId)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof commit !== 'undefined') { - payload['commit'] = commit; + apiPayload['commit'] = commit; } if (typeof rollback !== 'undefined') { - payload['rollback'] = rollback; + apiPayload['rollback'] = rollback; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -277,7 +293,7 @@ export class TablesDB extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -327,7 +343,7 @@ export class TablesDB extends Service { '{transactionId}', encodeURIComponent(String(transactionId)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; const uri = new URL(this.client.config.endpoint + apiPath); return this.client.call( @@ -338,7 +354,7 @@ export class TablesDB extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -404,10 +420,10 @@ export class TablesDB extends Service { '{transactionId}', encodeURIComponent(String(transactionId)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof operations !== 'undefined') { - payload['operations'] = operations; + apiPayload['operations'] = operations; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -419,7 +435,7 @@ export class TablesDB extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -532,22 +548,22 @@ export class TablesDB extends Service { const apiPath = '/tablesdb/{databaseId}/tables/{tableId}/rows' .replace('{databaseId}', encodeURIComponent(String(databaseId))) .replace('{tableId}', encodeURIComponent(String(tableId))); - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof queries !== 'undefined') { - payload['queries'] = queries; + apiPayload['queries'] = queries; } if (typeof transactionId !== 'undefined') { - payload['transactionId'] = transactionId; + apiPayload['transactionId'] = transactionId; } if (typeof total !== 'undefined') { - payload['total'] = total; + apiPayload['total'] = total; } if (typeof ttl !== 'undefined') { - payload['ttl'] = ttl; + apiPayload['ttl'] = ttl; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -558,7 +574,7 @@ export class TablesDB extends Service { 'X-Appwrite-Project': this.client.config.project, accept: 'application/json', }, - payload, + apiPayload, ); } @@ -699,22 +715,22 @@ export class TablesDB extends Service { const apiPath = '/tablesdb/{databaseId}/tables/{tableId}/rows' .replace('{databaseId}', encodeURIComponent(String(databaseId))) .replace('{tableId}', encodeURIComponent(String(tableId))); - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof rowId !== 'undefined') { - payload['rowId'] = rowId; + apiPayload['rowId'] = rowId; } if (typeof data !== 'undefined') { - payload['data'] = data; + apiPayload['data'] = data; } if (typeof permissions !== 'undefined') { - payload['permissions'] = permissions; + apiPayload['permissions'] = permissions; } if (typeof transactionId !== 'undefined') { - payload['transactionId'] = transactionId; + apiPayload['transactionId'] = transactionId; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -726,7 +742,7 @@ export class TablesDB extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -835,14 +851,14 @@ export class TablesDB extends Service { .replace('{databaseId}', encodeURIComponent(String(databaseId))) .replace('{tableId}', encodeURIComponent(String(tableId))) .replace('{rowId}', encodeURIComponent(String(rowId))); - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof queries !== 'undefined') { - payload['queries'] = queries; + apiPayload['queries'] = queries; } if (typeof transactionId !== 'undefined') { - payload['transactionId'] = transactionId; + apiPayload['transactionId'] = transactionId; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -853,7 +869,7 @@ export class TablesDB extends Service { 'X-Appwrite-Project': this.client.config.project, accept: 'application/json', }, - payload, + apiPayload, ); } @@ -994,18 +1010,18 @@ export class TablesDB extends Service { .replace('{databaseId}', encodeURIComponent(String(databaseId))) .replace('{tableId}', encodeURIComponent(String(tableId))) .replace('{rowId}', encodeURIComponent(String(rowId))); - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof data !== 'undefined') { - payload['data'] = data; + apiPayload['data'] = data; } if (typeof permissions !== 'undefined') { - payload['permissions'] = permissions; + apiPayload['permissions'] = permissions; } if (typeof transactionId !== 'undefined') { - payload['transactionId'] = transactionId; + apiPayload['transactionId'] = transactionId; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -1017,7 +1033,7 @@ export class TablesDB extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -1158,18 +1174,18 @@ export class TablesDB extends Service { .replace('{databaseId}', encodeURIComponent(String(databaseId))) .replace('{tableId}', encodeURIComponent(String(tableId))) .replace('{rowId}', encodeURIComponent(String(rowId))); - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof data !== 'undefined') { - payload['data'] = data; + apiPayload['data'] = data; } if (typeof permissions !== 'undefined') { - payload['permissions'] = permissions; + apiPayload['permissions'] = permissions; } if (typeof transactionId !== 'undefined') { - payload['transactionId'] = transactionId; + apiPayload['transactionId'] = transactionId; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -1181,7 +1197,7 @@ export class TablesDB extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -1281,10 +1297,10 @@ export class TablesDB extends Service { .replace('{databaseId}', encodeURIComponent(String(databaseId))) .replace('{tableId}', encodeURIComponent(String(tableId))) .replace('{rowId}', encodeURIComponent(String(rowId))); - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof transactionId !== 'undefined') { - payload['transactionId'] = transactionId; + apiPayload['transactionId'] = transactionId; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -1296,7 +1312,7 @@ export class TablesDB extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -1429,18 +1445,18 @@ export class TablesDB extends Service { .replace('{tableId}', encodeURIComponent(String(tableId))) .replace('{rowId}', encodeURIComponent(String(rowId))) .replace('{column}', encodeURIComponent(String(column))); - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof value !== 'undefined') { - payload['value'] = value; + apiPayload['value'] = value; } if (typeof min !== 'undefined') { - payload['min'] = min; + apiPayload['min'] = min; } if (typeof transactionId !== 'undefined') { - payload['transactionId'] = transactionId; + apiPayload['transactionId'] = transactionId; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -1452,7 +1468,7 @@ export class TablesDB extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -1585,18 +1601,18 @@ export class TablesDB extends Service { .replace('{tableId}', encodeURIComponent(String(tableId))) .replace('{rowId}', encodeURIComponent(String(rowId))) .replace('{column}', encodeURIComponent(String(column))); - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof value !== 'undefined') { - payload['value'] = value; + apiPayload['value'] = value; } if (typeof max !== 'undefined') { - payload['max'] = max; + apiPayload['max'] = max; } if (typeof transactionId !== 'undefined') { - payload['transactionId'] = transactionId; + apiPayload['transactionId'] = transactionId; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -1608,7 +1624,7 @@ export class TablesDB extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } } diff --git a/src/services/teams.ts b/src/services/teams.ts index 4bc7e806..a4424fcd 100644 --- a/src/services/teams.ts +++ b/src/services/teams.ts @@ -70,18 +70,18 @@ export class Teams extends Service { const total = params.total; const apiPath = '/teams'; - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof queries !== 'undefined') { - payload['queries'] = queries; + apiPayload['queries'] = queries; } if (typeof search !== 'undefined') { - payload['search'] = search; + apiPayload['search'] = search; } if (typeof total !== 'undefined') { - payload['total'] = total; + apiPayload['total'] = total; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -92,7 +92,7 @@ export class Teams extends Service { 'X-Appwrite-Project': this.client.config.project, accept: 'application/json', }, - payload, + apiPayload, ); } @@ -101,7 +101,7 @@ export class Teams extends Service { * * @param {string} params.teamId - Team ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can't start with a special char. Max length is 36 chars. * @param {string} params.name - Team name. Max length: 128 chars. - * @param {string[]} params.roles - Array of strings. Use this param to set the roles in the team for the user who created it. The default role is **owner**. A role can be any string. Learn more about [roles and permissions](https://appwrite.io/docs/permissions). Maximum of 100 roles are allowed, each 32 characters long. + * @param {string[]} params.roles - Array of strings. Use this param to set the roles in the team for the user who created it. The default role is **owner**. A role can be any string. Learn more about [roles and permissions](https://appwrite.io/docs/permissions). Maximum of 100 roles are allowed, each 81 characters long. * @throws {AppwriteException} * @returns {Promise} */ @@ -117,7 +117,7 @@ export class Teams extends Service { * * @param {string} teamId - Team ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can't start with a special char. Max length is 36 chars. * @param {string} name - Team name. Max length: 128 chars. - * @param {string[]} roles - Array of strings. Use this param to set the roles in the team for the user who created it. The default role is **owner**. A role can be any string. Learn more about [roles and permissions](https://appwrite.io/docs/permissions). Maximum of 100 roles are allowed, each 32 characters long. + * @param {string[]} roles - Array of strings. Use this param to set the roles in the team for the user who created it. The default role is **owner**. A role can be any string. Learn more about [roles and permissions](https://appwrite.io/docs/permissions). Maximum of 100 roles are allowed, each 81 characters long. * @throws {AppwriteException} * @returns {Promise>} * @deprecated Use the object parameter style method for a better developer experience. @@ -165,18 +165,18 @@ export class Teams extends Service { } const apiPath = '/teams'; - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof teamId !== 'undefined') { - payload['teamId'] = teamId; + apiPayload['teamId'] = teamId; } if (typeof name !== 'undefined') { - payload['name'] = name; + apiPayload['name'] = name; } if (typeof roles !== 'undefined') { - payload['roles'] = roles; + apiPayload['roles'] = roles; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -188,7 +188,7 @@ export class Teams extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -240,7 +240,7 @@ export class Teams extends Service { '{teamId}', encodeURIComponent(String(teamId)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; const uri = new URL(this.client.config.endpoint + apiPath); return this.client.call( @@ -250,7 +250,7 @@ export class Teams extends Service { 'X-Appwrite-Project': this.client.config.project, accept: 'application/json', }, - payload, + apiPayload, ); } @@ -316,10 +316,10 @@ export class Teams extends Service { '{teamId}', encodeURIComponent(String(teamId)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof name !== 'undefined') { - payload['name'] = name; + apiPayload['name'] = name; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -331,7 +331,7 @@ export class Teams extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -377,7 +377,7 @@ export class Teams extends Service { '{teamId}', encodeURIComponent(String(teamId)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; const uri = new URL(this.client.config.endpoint + apiPath); return this.client.call( @@ -388,7 +388,7 @@ export class Teams extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -458,14 +458,14 @@ export class Teams extends Service { '{teamId}', encodeURIComponent(String(teamId)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof queries !== 'undefined') { - payload['queries'] = queries; + apiPayload['queries'] = queries; } if (typeof total !== 'undefined') { - payload['total'] = total; + apiPayload['total'] = total; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -476,7 +476,7 @@ export class Teams extends Service { 'X-Appwrite-Project': this.client.config.project, accept: 'application/json', }, - payload, + apiPayload, ); } @@ -555,14 +555,14 @@ export class Teams extends Service { '{teamId}', encodeURIComponent(String(teamId)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof appId !== 'undefined') { - payload['appId'] = appId; + apiPayload['appId'] = appId; } if (typeof authorizationDetails !== 'undefined') { - payload['authorizationDetails'] = authorizationDetails; + apiPayload['authorizationDetails'] = authorizationDetails; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -574,7 +574,7 @@ export class Teams extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -644,7 +644,7 @@ export class Teams extends Service { '{installationId}', encodeURIComponent(String(installationId)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; const uri = new URL(this.client.config.endpoint + apiPath); return this.client.call( @@ -654,7 +654,7 @@ export class Teams extends Service { 'X-Appwrite-Project': this.client.config.project, accept: 'application/json', }, - payload, + apiPayload, ); } @@ -741,10 +741,10 @@ export class Teams extends Service { '{installationId}', encodeURIComponent(String(installationId)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof authorizationDetails !== 'undefined') { - payload['authorizationDetails'] = authorizationDetails; + apiPayload['authorizationDetails'] = authorizationDetails; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -756,7 +756,7 @@ export class Teams extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -823,7 +823,7 @@ export class Teams extends Service { '{installationId}', encodeURIComponent(String(installationId)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; const uri = new URL(this.client.config.endpoint + apiPath); return this.client.call( @@ -834,7 +834,7 @@ export class Teams extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -922,18 +922,18 @@ export class Teams extends Service { '{teamId}', encodeURIComponent(String(teamId)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof queries !== 'undefined') { - payload['queries'] = queries; + apiPayload['queries'] = queries; } if (typeof search !== 'undefined') { - payload['search'] = search; + apiPayload['search'] = search; } if (typeof total !== 'undefined') { - payload['total'] = total; + apiPayload['total'] = total; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -944,7 +944,7 @@ export class Teams extends Service { 'X-Appwrite-Project': this.client.config.project, accept: 'application/json', }, - payload, + apiPayload, ); } @@ -1077,30 +1077,30 @@ export class Teams extends Service { '{teamId}', encodeURIComponent(String(teamId)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof email !== 'undefined') { - payload['email'] = email; + apiPayload['email'] = email; } if (typeof userId !== 'undefined') { - payload['userId'] = userId; + apiPayload['userId'] = userId; } if (typeof phone !== 'undefined') { - payload['phone'] = phone; + apiPayload['phone'] = phone; } if (typeof roles !== 'undefined') { - payload['roles'] = roles; + apiPayload['roles'] = roles; } if (typeof url !== 'undefined') { - payload['url'] = url; + apiPayload['url'] = url; } if (typeof name !== 'undefined') { - payload['name'] = name; + apiPayload['name'] = name; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -1112,7 +1112,7 @@ export class Teams extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -1182,7 +1182,7 @@ export class Teams extends Service { '{membershipId}', encodeURIComponent(String(membershipId)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; const uri = new URL(this.client.config.endpoint + apiPath); return this.client.call( @@ -1192,7 +1192,7 @@ export class Teams extends Service { 'X-Appwrite-Project': this.client.config.project, accept: 'application/json', }, - payload, + apiPayload, ); } @@ -1276,10 +1276,10 @@ export class Teams extends Service { '{membershipId}', encodeURIComponent(String(membershipId)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof roles !== 'undefined') { - payload['roles'] = roles; + apiPayload['roles'] = roles; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -1291,7 +1291,7 @@ export class Teams extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -1358,7 +1358,7 @@ export class Teams extends Service { '{membershipId}', encodeURIComponent(String(membershipId)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; const uri = new URL(this.client.config.endpoint + apiPath); return this.client.call( @@ -1369,7 +1369,7 @@ export class Teams extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -1479,14 +1479,14 @@ export class Teams extends Service { '{membershipId}', encodeURIComponent(String(membershipId)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof userId !== 'undefined') { - payload['userId'] = userId; + apiPayload['userId'] = userId; } if (typeof secret !== 'undefined') { - payload['secret'] = secret; + apiPayload['secret'] = secret; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -1498,7 +1498,7 @@ export class Teams extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -1550,7 +1550,7 @@ export class Teams extends Service { '{teamId}', encodeURIComponent(String(teamId)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; const uri = new URL(this.client.config.endpoint + apiPath); return this.client.call( @@ -1560,7 +1560,7 @@ export class Teams extends Service { 'X-Appwrite-Project': this.client.config.project, accept: 'application/json', }, - payload, + apiPayload, ); } @@ -1623,10 +1623,10 @@ export class Teams extends Service { '{teamId}', encodeURIComponent(String(teamId)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof prefs !== 'undefined') { - payload['prefs'] = prefs; + apiPayload['prefs'] = prefs; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -1638,7 +1638,7 @@ export class Teams extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } } diff --git a/src/services/vectors-db.ts b/src/services/vectors-db.ts index 02fc557a..710e8b44 100644 --- a/src/services/vectors-db.ts +++ b/src/services/vectors-db.ts @@ -12,46 +12,62 @@ export class VectorsDB extends Service { * List transactions across all databases. * * @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). + * @param {boolean} params.total - When set to false, the total count returned will be 0 and will not be calculated. * @throws {AppwriteException} * @returns {Promise} */ listTransactions(params?: { queries?: string[]; + total?: boolean; }): Promise; /** * List transactions across all databases. * * @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). + * @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. */ - listTransactions(queries?: string[]): Promise; listTransactions( - paramsOrFirst?: { queries?: string[] } | string[], + queries?: string[], + total?: boolean, + ): Promise; + listTransactions( + paramsOrFirst?: { queries?: string[]; total?: boolean } | string[], + ...rest: [boolean?] ): Promise { - let params: { queries?: string[] }; + let params: { queries?: string[]; total?: boolean }; if ( - typeof paramsOrFirst === 'undefined' || + (typeof paramsOrFirst === 'undefined' && rest.length === 0) || (paramsOrFirst && typeof paramsOrFirst === 'object' && !Array.isArray(paramsOrFirst)) ) { - params = (paramsOrFirst || {}) as { queries?: string[] }; + 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 = '/vectorsdb/transactions'; - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof queries !== 'undefined') { - payload['queries'] = queries; + apiPayload['queries'] = queries; + } + + if (typeof total !== 'undefined') { + apiPayload['total'] = total; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -62,7 +78,7 @@ export class VectorsDB extends Service { 'X-Appwrite-Project': this.client.config.project, accept: 'application/json', }, - payload, + apiPayload, ); } @@ -104,10 +120,10 @@ export class VectorsDB extends Service { const ttl = params.ttl; const apiPath = '/vectorsdb/transactions'; - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof ttl !== 'undefined') { - payload['ttl'] = ttl; + apiPayload['ttl'] = ttl; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -119,7 +135,7 @@ export class VectorsDB extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -171,7 +187,7 @@ export class VectorsDB extends Service { '{transactionId}', encodeURIComponent(String(transactionId)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; const uri = new URL(this.client.config.endpoint + apiPath); return this.client.call( @@ -181,7 +197,7 @@ export class VectorsDB extends Service { 'X-Appwrite-Project': this.client.config.project, accept: 'application/json', }, - payload, + apiPayload, ); } @@ -258,14 +274,14 @@ export class VectorsDB extends Service { '{transactionId}', encodeURIComponent(String(transactionId)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof commit !== 'undefined') { - payload['commit'] = commit; + apiPayload['commit'] = commit; } if (typeof rollback !== 'undefined') { - payload['rollback'] = rollback; + apiPayload['rollback'] = rollback; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -277,7 +293,7 @@ export class VectorsDB extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -327,7 +343,7 @@ export class VectorsDB extends Service { '{transactionId}', encodeURIComponent(String(transactionId)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; const uri = new URL(this.client.config.endpoint + apiPath); return this.client.call( @@ -338,7 +354,7 @@ export class VectorsDB extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -404,10 +420,10 @@ export class VectorsDB extends Service { '{transactionId}', encodeURIComponent(String(transactionId)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof operations !== 'undefined') { - payload['operations'] = operations; + apiPayload['operations'] = operations; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -419,7 +435,7 @@ export class VectorsDB extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -538,22 +554,22 @@ export class VectorsDB extends Service { '{collectionId}', encodeURIComponent(String(collectionId)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof queries !== 'undefined') { - payload['queries'] = queries; + apiPayload['queries'] = queries; } if (typeof transactionId !== 'undefined') { - payload['transactionId'] = transactionId; + apiPayload['transactionId'] = transactionId; } if (typeof total !== 'undefined') { - payload['total'] = total; + apiPayload['total'] = total; } if (typeof ttl !== 'undefined') { - payload['ttl'] = ttl; + apiPayload['ttl'] = ttl; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -564,7 +580,7 @@ export class VectorsDB extends Service { 'X-Appwrite-Project': this.client.config.project, accept: 'application/json', }, - payload, + apiPayload, ); } @@ -718,22 +734,22 @@ export class VectorsDB extends Service { '{collectionId}', encodeURIComponent(String(collectionId)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof documentId !== 'undefined') { - payload['documentId'] = documentId; + apiPayload['documentId'] = documentId; } if (typeof data !== 'undefined') { - payload['data'] = data; + apiPayload['data'] = data; } if (typeof permissions !== 'undefined') { - payload['permissions'] = permissions; + apiPayload['permissions'] = permissions; } if (typeof transactionId !== 'undefined') { - payload['transactionId'] = transactionId; + apiPayload['transactionId'] = transactionId; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -745,7 +761,7 @@ export class VectorsDB extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -866,22 +882,22 @@ export class VectorsDB extends Service { '{collectionId}', encodeURIComponent(String(collectionId)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof queries !== 'undefined') { - payload['queries'] = queries; + apiPayload['queries'] = queries; } if (typeof transactionId !== 'undefined') { - payload['transactionId'] = transactionId; + apiPayload['transactionId'] = transactionId; } if (typeof total !== 'undefined') { - payload['total'] = total; + apiPayload['total'] = total; } if (typeof ttl !== 'undefined') { - payload['ttl'] = ttl; + apiPayload['ttl'] = ttl; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -893,7 +909,7 @@ export class VectorsDB extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -1013,14 +1029,14 @@ export class VectorsDB extends Service { '{documentId}', encodeURIComponent(String(documentId)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof queries !== 'undefined') { - payload['queries'] = queries; + apiPayload['queries'] = queries; } if (typeof transactionId !== 'undefined') { - payload['transactionId'] = transactionId; + apiPayload['transactionId'] = transactionId; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -1031,7 +1047,7 @@ export class VectorsDB extends Service { 'X-Appwrite-Project': this.client.config.project, accept: 'application/json', }, - payload, + apiPayload, ); } @@ -1187,18 +1203,18 @@ export class VectorsDB extends Service { '{documentId}', encodeURIComponent(String(documentId)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof data !== 'undefined') { - payload['data'] = data; + apiPayload['data'] = data; } if (typeof permissions !== 'undefined') { - payload['permissions'] = permissions; + apiPayload['permissions'] = permissions; } if (typeof transactionId !== 'undefined') { - payload['transactionId'] = transactionId; + apiPayload['transactionId'] = transactionId; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -1210,7 +1226,7 @@ export class VectorsDB extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -1366,18 +1382,18 @@ export class VectorsDB extends Service { '{documentId}', encodeURIComponent(String(documentId)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof data !== 'undefined') { - payload['data'] = data; + apiPayload['data'] = data; } if (typeof permissions !== 'undefined') { - payload['permissions'] = permissions; + apiPayload['permissions'] = permissions; } if (typeof transactionId !== 'undefined') { - payload['transactionId'] = transactionId; + apiPayload['transactionId'] = transactionId; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -1389,7 +1405,7 @@ export class VectorsDB extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } @@ -1498,10 +1514,10 @@ export class VectorsDB extends Service { '{documentId}', encodeURIComponent(String(documentId)), ); - const payload: Payload = {}; + const apiPayload: Payload = {}; if (typeof transactionId !== 'undefined') { - payload['transactionId'] = transactionId; + apiPayload['transactionId'] = transactionId; } const uri = new URL(this.client.config.endpoint + apiPath); @@ -1513,7 +1529,7 @@ export class VectorsDB extends Service { 'content-type': 'application/json', accept: 'application/json', }, - payload, + apiPayload, ); } } diff --git a/src/topic.ts b/src/topic.ts new file mode 100644 index 00000000..cd6cea01 --- /dev/null +++ b/src/topic.ts @@ -0,0 +1,103 @@ +/** + * Validate the levels passed to `path()`: a non-empty array of non-empty strings, none holding + * the level separator `/` or the wildcards `+` and `#` (use `any()` and `all()` for those). + */ +function levelsOf(levels: unknown): string[] { + if (!Array.isArray(levels)) { + throw new Error("path() expects an array, e.g. path(['user'])"); + } + if (levels.length === 0) { + throw new Error('path() needs at least one level'); + } + return levels.map((level: unknown, index: number) => { + if (typeof level !== 'string') { + const type = level === null ? 'null' : typeof level; + throw new Error( + `level at index ${index} must be a string, got ${type}`, + ); + } + if (level === '') { + throw new Error(`empty level at index ${index}`); + } + if (level.includes('/')) { + const parts = level + .split('/') + .map((part) => `'${part}'`) + .join(', '); + throw new Error( + `"${level}" contains "/", split it: path([${parts}])`, + ); + } + if (level === '+') { + throw new Error('"+" is reserved, use any()'); + } + if (level.includes('+')) { + throw new Error(`"${level}" contains "+", use any() for wildcards`); + } + if (level === '#') { + throw new Error('"#" is reserved, use all()'); + } + if (level.includes('#')) { + throw new Error(`"${level}" contains "#", use all() for wildcards`); + } + return level; + }); +} + +/** + * Helper to build MQTT topics for `Push`, level by level. + * + * Topic.path(['user', userId, 'notification']); // "user/123/notification" + * Topic.path(['user']).any().path(['notification']); // "user/+/notification" + * Topic.path(['org']).any().path(['logs']).all(); // "org/+/logs/#" + * + * `any()` matches exactly one level and `all()` everything below, at any depth (including the + * parent itself). `all()` ends the topic, so it returns a {@link ResolvedTopic}. + */ +export class Topic { + private constructor(private readonly levels: readonly string[]) {} + + /** Start a topic with the given levels, e.g. `Topic.path(['user', userId])`. */ + static path(levels: string[]): Topic { + return new Topic(levelsOf(levels)); + } + + /** Start a topic with a single-level wildcard (`+`). */ + static any(): Topic { + return new Topic(['+']); + } + + /** The multi-level wildcard alone (`#`): matches every topic. */ + static all(): ResolvedTopic { + return new ResolvedTopic(['#']); + } + + /** Append the given levels. */ + path(levels: string[]): Topic { + return new Topic([...this.levels, ...levelsOf(levels)]); + } + + /** Append a single-level wildcard (`+`): matches exactly one level. */ + any(): Topic { + return new Topic([...this.levels, '+']); + } + + /** Append the multi-level wildcard (`#`) and end the topic. */ + all(): ResolvedTopic { + return new ResolvedTopic([...this.levels, '#']); + } + + toString(): string { + return this.levels.join('/'); + } +} + +/** A topic ended by `all()`: nothing can be appended after the `#` wildcard. */ +export class ResolvedTopic { + /** @internal Created by {@link Topic.all}. */ + constructor(private readonly levels: readonly string[]) {} + + toString(): string { + return this.levels.join('/'); + } +}