Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions appwrite-client/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ kotlin {
iosArm64()
iosSimulatorArm64()

@OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class)
wasmJs {
browser()
nodejs()
}

compilerOptions {
freeCompilerArgs.add("-Xexpect-actual-classes")
}
Expand All @@ -47,6 +53,9 @@ kotlin {
iosMain.dependencies {
implementation(libs.ktor.client.darwin)
}
wasmJsMain.dependencies {
implementation(libs.ktor.client.js)
}
commonTest.dependencies {
implementation(kotlin("test"))
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package io.appwrite.client.persistence

/**
* wasmJs session persistence backed by the browser `window.localStorage`.
*
* Implemented via small `js(...)` shims so the module does not need the
* optional `kotlinx-browser` DOM artifact on its classpath.
*/
actual class SessionStore actual constructor() {
private val prefix = "io.appwrite.session."

actual fun save(key: String, value: String) {
lsSet(prefix + key, value)
}

actual fun load(key: String): String? = lsGet(prefix + key)

actual fun remove(key: String) {
lsRemove(prefix + key)
}

actual fun clear() {
val count = lsLength()
val keys = buildList {
for (i in 0 until count) {
lsKey(i)?.let { add(it) }
}
}
keys.filter { it.startsWith(prefix) }.forEach { lsRemove(it) }
}
}

private fun lsSet(key: String, value: String): Unit =
js("{ localStorage.setItem(key, value); }")

private fun lsGet(key: String): String? =
js("localStorage.getItem(key)")

private fun lsRemove(key: String): Unit =
js("{ localStorage.removeItem(key); }")

private fun lsLength(): Int =
js("localStorage.length")

private fun lsKey(index: Int): String? =
js("localStorage.key(index)")
6 changes: 6 additions & 0 deletions appwrite-core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ kotlin {
iosArm64()
iosSimulatorArm64()

@OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class)
wasmJs {
browser()
nodejs()
}

sourceSets {
commonMain.dependencies {
implementation(libs.kotlinx.serialization.json)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package io.appwrite.core.permission

/**
* Permission helper mirroring the Appwrite Web SDK `Permission` class.
*
* Combine with [Role] to build permission strings:
*
* ```
* permissions = listOf(
* Permission.read(Role.any()),
* Permission.update(Role.user("507f1f77bcf86cd799439011")),
* Permission.delete(Role.team("admins", "owner")),
* )
* ```
*/
public object Permission {

public fun read(role: String): String = "read(\"$role\")"

public fun write(role: String): String = "write(\"$role\")"

public fun create(role: String): String = "create(\"$role\")"

public fun update(role: String): String = "update(\"$role\")"

public fun delete(role: String): String = "delete(\"$role\")"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package io.appwrite.core.permission

/**
* Role helper mirroring the Appwrite Web SDK `Role` class. Produces the role
* strings consumed by [Permission].
*
* ```
* Role.any() // "any"
* Role.user("id") // "user:id"
* Role.user("id", "verified") // "user:id/verified"
* Role.users() // "users"
* Role.users("verified") // "users/verified"
* Role.guests() // "guests"
* Role.team("id") // "team:id"
* Role.team("id", "owner") // "team:id/owner"
* Role.member("id") // "member:id"
* Role.label("admin") // "label:admin"
* ```
*/
public object Role {

/** Grants access to anyone. */
public fun any(): String = "any"

/** Grants access to a specific user by ID, optionally scoped to a [status]. */
public fun user(id: String, status: String = ""): String =
if (status.isEmpty()) "user:$id" else "user:$id/$status"

/** Grants access to all users, optionally scoped to a [status] (e.g. "verified"). */
public fun users(status: String = ""): String =
if (status.isEmpty()) "users" else "users/$status"

/** Grants access to unauthenticated (guest) users. */
public fun guests(): String = "guests"

/** Grants access to a team by ID, optionally scoped to a team [role]. */
public fun team(id: String, role: String = ""): String =
if (role.isEmpty()) "team:$id" else "team:$id/$role"

/** Grants access to a specific team member by membership ID. */
public fun member(id: String): String = "member:$id"

/** Grants access to users carrying a specific [name] label. */
public fun label(name: String): String = "label:$name"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
package io.appwrite.core.query

import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonNull
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.add
import kotlinx.serialization.json.buildJsonArray
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put

/**
* Static query builder that mirrors the Appwrite Web/JS SDK `Query` class.
*
* Each helper returns a JSON-encoded string in the modern Appwrite wire format:
*
* ```json
* {"method":"equal","attribute":"name","values":["Alice"]}
* ```
*
* This is the format required by the `x-appwrite-response-format: 1.8.0`
* endpoints. Usage mirrors the JS SDK one-to-one:
*
* ```
* databases.listDocuments(db, col, queries = listOf(
* Query.equal("status", "active"),
* Query.greaterThan("age", 18),
* Query.orderDesc("createdAt"),
* Query.limit(25),
* ))
* ```
*
* For a type-safe Kotlin DSL alternative see [QueryBuilder] / [buildQuery].
*/
public object Query {

private val json: Json = Json

// ── Comparison ───────────────────────────────────────────────────

public fun equal(attribute: String, value: Any): String = encode("equal", attribute, value)

public fun equal(attribute: String, values: List<Any>): String = encode("equal", attribute, values)

public fun notEqual(attribute: String, value: Any): String = encode("notEqual", attribute, value)

public fun lessThan(attribute: String, value: Any): String = encode("lessThan", attribute, value)

public fun lessThanEqual(attribute: String, value: Any): String = encode("lessThanEqual", attribute, value)

public fun greaterThan(attribute: String, value: Any): String = encode("greaterThan", attribute, value)

public fun greaterThanEqual(attribute: String, value: Any): String = encode("greaterThanEqual", attribute, value)

public fun between(attribute: String, start: Any, end: Any): String =
encode("between", attribute, listOf(start, end))

public fun notBetween(attribute: String, start: Any, end: Any): String =
encode("notBetween", attribute, listOf(start, end))

// ── Null checks ──────────────────────────────────────────────────

public fun isNull(attribute: String): String = encode("isNull", attribute, null)

public fun isNotNull(attribute: String): String = encode("isNotNull", attribute, null)

// ── String operations ────────────────────────────────────────────

public fun startsWith(attribute: String, value: String): String = encode("startsWith", attribute, value)

public fun notStartsWith(attribute: String, value: String): String = encode("notStartsWith", attribute, value)

public fun endsWith(attribute: String, value: String): String = encode("endsWith", attribute, value)

public fun notEndsWith(attribute: String, value: String): String = encode("notEndsWith", attribute, value)

public fun contains(attribute: String, value: String): String = encode("contains", attribute, value)

public fun contains(attribute: String, values: List<Any>): String = encode("contains", attribute, values)

public fun notContains(attribute: String, value: String): String = encode("notContains", attribute, value)

public fun notContains(attribute: String, values: List<Any>): String = encode("notContains", attribute, values)

public fun search(attribute: String, value: String): String = encode("search", attribute, value)

public fun notSearch(attribute: String, value: String): String = encode("notSearch", attribute, value)

// ── Selection / ordering ─────────────────────────────────────────

public fun select(attributes: List<String>): String = encode("select", null, attributes)

public fun select(vararg attributes: String): String = select(attributes.toList())

public fun orderAsc(attribute: String): String = encode("orderAsc", attribute, null)

public fun orderDesc(attribute: String): String = encode("orderDesc", attribute, null)

public fun orderRandom(): String = encode("orderRandom", null, null)

// ── Pagination ───────────────────────────────────────────────────

public fun cursorAfter(documentId: String): String = encode("cursorAfter", null, documentId)

public fun cursorBefore(documentId: String): String = encode("cursorBefore", null, documentId)

public fun limit(limit: Int): String = encode("limit", null, limit)

public fun offset(offset: Int): String = encode("offset", null, offset)

// ── Time helpers ─────────────────────────────────────────────────

public fun createdBefore(value: String): String = encode("createdBefore", null, value)

public fun createdAfter(value: String): String = encode("createdAfter", null, value)

public fun updatedBefore(value: String): String = encode("updatedBefore", null, value)

public fun updatedAfter(value: String): String = encode("updatedAfter", null, value)

// ── Logical operators ────────────────────────────────────────────

/** Combine sub-queries (already-encoded JSON strings) with logical AND. */
public fun and(queries: List<String>): String = encodeNested("and", queries)

/** Combine sub-queries (already-encoded JSON strings) with logical OR. */
public fun or(queries: List<String>): String = encodeNested("or", queries)

// ── Internals ────────────────────────────────────────────────────

private fun encode(method: String, attribute: String?, value: Any?): String {
val obj = buildJsonObject {
put("method", method)
if (attribute != null) put("attribute", attribute)
if (value != null) {
put("values", valuesArray(value))
}
}
return json.encodeToString(JsonObject.serializer(), obj)
}

private fun encodeNested(method: String, queries: List<String>): String {
val obj = buildJsonObject {
put("method", method)
put(
"values",
buildJsonArray {
queries.forEach { q -> add(json.parseToJsonElement(q)) }
},
)
}
return json.encodeToString(JsonObject.serializer(), obj)
}

/** Normalise a single value or a list into the `values` JSON array. */
private fun valuesArray(value: Any): JsonArray = when (value) {
is List<*> -> buildJsonArray { value.forEach { add(toElement(it)) } }
else -> buildJsonArray { add(toElement(value)) }
}

private fun toElement(value: Any?): JsonElement = when (value) {
null -> JsonNull
is JsonElement -> value
is String -> JsonPrimitive(value)
is Boolean -> JsonPrimitive(value)
is Int -> JsonPrimitive(value)
is Long -> JsonPrimitive(value)
is Double -> JsonPrimitive(value)
is Float -> JsonPrimitive(value)
is Number -> JsonPrimitive(value)
else -> JsonPrimitive(value.toString())
}
}
44 changes: 44 additions & 0 deletions appwrite-core/src/commonMain/kotlin/io/appwrite/core/types/ID.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package io.appwrite.core.types

import kotlinx.datetime.Clock
import kotlin.random.Random

/**
* ID helper mirroring the Appwrite Web SDK `ID` class.
*
* ```
* ID.unique() // server-friendly time-ordered unique id
* ID.custom("my-id") // pass-through of a caller-provided id
* ```
*
* [unique] reproduces the JS SDK algorithm: a hex-encoded Unix-seconds prefix,
* a 5-character hex millisecond component, and [padding] random hex characters.
*/
public object ID {

/** Returns [id] unchanged — use for caller-supplied identifiers. */
public fun custom(id: String): String = id

/**
* Generates a unique, roughly time-ordered ID with [padding] trailing
* random hex characters (default 7, matching the JS SDK).
*/
public fun unique(padding: Int = 7): String {
val now = Clock.System.now()
val seconds = now.epochSeconds
val millis = (now.nanosecondsOfSecond / 1_000_000)

val baseId = seconds.toString(16)
// 5-character zero-padded hex of the millisecond component.
val msHex = millis.toString(16).padStart(5, '0')

val random = buildString {
repeat(padding) {
append(HEX[Random.nextInt(16)])
}
}
return baseId + msHex + random
}

private const val HEX = "0123456789abcdef"
}
Loading
Loading