mcpField
A
appwrite-kotlin

appwrite-kotlin

github.com/appwrite開発28865 回インストール1 週間前に更新
リポジトリ

appwrite-kotlin について

appwrite-kotlin は、Android と Kotlin JVM アプリ向けの Appwrite SDK です。クライアントとサーバーの開発者が、認証、データベース、ストレージ、リアルタイム購読、関数実行を利用でき、SSR 認証の例も含みます。

主な機能

  1. 01ユーザー認証と管理
  2. 02TablesDB の操作とクエリ
  3. 03ファイルストレージと権限設定
  4. 04チーム管理とリアルタイム購読
  5. 05Appwrite 関数の実行

活用シーン

  1. 01Android アプリへの Appwrite 認証の追加
  2. 02Kotlin サーバーでのユーザーとデータの管理
  3. 03行やファイルのリアルタイム変更の購読

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/appwrite/skills --skill appwrite-kotlin

インストール後、Claude Code で/appwrite-kotlinを入力して読み込まれたか確認します。

詳細

Appwrite Kotlin SDK

インストール

// build.gradle.kts — Android
implementation("io.appwrite:sdk-for-android:+")

// build.gradle.kts — Server (Kotlin JVM)
implementation("io.appwrite:sdk-for-kotlin:+")

クライアントのセットアップ

クライアントサイド(Android)
import io.appwrite.Client
import io.appwrite.ID
import io.appwrite.Query
import io.appwrite.enums.OAuthProvider
import io.appwrite.services.Account
import io.appwrite.services.Realtime
import io.appwrite.services.TablesDB
import io.appwrite.services.Storage
import io.appwrite.models.InputFile

val client = Client(context)
    .setEndpoint("https://<REGION>.cloud.appwrite.io/v1")
    .setProject("[PROJECT_ID]")
サーバーサイド(Kotlin JVM)
import io.appwrite.Client
import io.appwrite.ID
import io.appwrite.Query
import io.appwrite.services.Users
import io.appwrite.services.TablesDB
import io.appwrite.services.Storage
import io.appwrite.services.Functions

val client = Client()
    .setEndpoint("https://<REGION>.cloud.appwrite.io/v1")
    .setProject(System.getenv("APPWRITE_PROJECT_ID"))
    .setKey(System.getenv("APPWRITE_API_KEY"))

コード例

認証(クライアントサイド)
val account = Account(client)

// Signup
account.create(
    userId = ID.unique(),
    email = "[email protected]",
    password = "password123",
    name = "User Name"
)

// Login
val session = account.createEmailPasswordSession(
    email = "[email protected]",
    password = "password123"
)

// OAuth
account.createOAuth2Session(activity = activity, provider = OAuthProvider.GOOGLE)

// Get current user
val user = account.get()

// Logout
account.deleteSession(sessionId = "current")
ユーザー管理(サーバーサイド)
val users = Users(client)

// Create user
val user = users.create(
    userId = ID.unique(),
    email = "[email protected]",
    password = "password123",
    name = "User Name"
)

// List users
val list = users.list()

// Get user
val fetched = users.get(userId = "[USER_ID]")

// Delete user
users.delete(userId = "[USER_ID]")
データベース操作

Note: すべての新しいコードでは、TablesDB(非推奨の Databases クラスではない)を使用してください。既存のコードベースがすでに Databases に依存している場合、またはユーザーが明示的に要求した場合にのみ Databases を使用してください。

Tip: すべての SDK メソッド呼び出しでは、名前付き引数(例: databaseId = "...")を優先してください。既存のコードベースがすでに位置引数を使用している場合、またはユーザーが明示的に要求した場合にのみ、位置引数を使用してください。

val tablesDB = TablesDB(client)

// Create database (server-side only)
val db = tablesDB.create(databaseId = ID.unique(), name = "My Database")

// Create row
val doc = tablesDB.createRow(
    databaseId = "[DATABASE_ID]",
    tableId = "[TABLE_ID]",
    rowId = ID.unique(),
    data = mapOf("title" to "Hello", "done" to false)
)

// Query rows
val results = tablesDB.listRows(
    databaseId = "[DATABASE_ID]",
    tableId = "[TABLE_ID]",
    queries = listOf(Query.equal("done", false), Query.limit(10))
)

// Get row
val row = tablesDB.getRow(databaseId = "[DATABASE_ID]", tableId = "[TABLE_ID]", rowId = "[ROW_ID]")

// Update row
tablesDB.updateRow(
    databaseId = "[DATABASE_ID]",
    tableId = "[TABLE_ID]",
    rowId = "[ROW_ID]",
    data = mapOf("done" to true)
)

// Delete row
tablesDB.deleteRow(
    databaseId = "[DATABASE_ID]",
    tableId = "[TABLE_ID]",
    rowId = "[ROW_ID]"
)
文字列カラム型

Note: 従来の string 型は非推奨です。すべての新しいカラムには明示的なカラム型を使用してください。

型最大文字数インデックスストレージ
varchar16,383完全インデックス(サイズが 768 以下の場合)行内にインライン保存
text16,383プレフィックスのみオフページ
mediumtext4,194,303プレフィックスのみオフページ
longtext1,073,741,823プレフィックスのみオフページ
  • varchar はインラインで保存され、64 KB の行サイズ制限にカウントされます。名前、スラッグ、識別子などの短くインデックスされたフィールドに適しています。
  • text、mediumtext、longtext はオフページで保存されるため(行内には 20 バイトのポインタのみが存在します)、行サイズの予算を消費しません。これらの型には size は必要ありません。
// Create table with explicit string column types
tablesDB.createTable(
    databaseId = "[DATABASE_ID]",
    tableId = ID.unique(),
    name = "articles",
    columns = listOf(
        mapOf("key" to "title",    "type" to "varchar",    "size" to 255, "required" to true),
        mapOf("key" to "summary",  "type" to "text",                      "required" to false),
        mapOf("key" to "body",     "type" to "mediumtext",                "required" to false),
        mapOf("key" to "raw_data", "type" to "longtext",                  "required" to false),
    )
)
クエリメソッド
// Filtering
Query.equal("field", "value")             // == (or pass list for IN)
Query.notEqual("field", "value")          // !=
Query.lessThan("field", 100)              // <
Query.lessThanEqual("field", 100)         // <=
Query.greaterThan("field", 100)           // >
Query.greaterThanEqual("field", 100)      // >=
Query.between("field", 1, 100)            // 1 <= field <= 100
Query.isNull("field")                     // is null
Query.isNotNull("field")                  // is not null
Query.startsWith("field", "prefix")       // starts with
Query.endsWith("field", "suffix")         // ends with
Query.contains("field", "sub")            // contains
Query.search("field", "keywords")         // full-text search (requires index)

// Sorting
Query.orderAsc("field")
Query.orderDesc("field")

// Pagination
Query.limit(25)                           // max rows (default 25, max 100)
Query.offset(0)                           // skip N rows
Query.cursorAfter("[ROW_ID]")             // cursor pagination (preferred)
Query.cursorBefore("[ROW_ID]")

// Selection & Logic
Query.select(listOf("field1", "field2"))
Query.or(listOf(Query.equal("a", 1), Query.equal("b", 2)))   // OR
Query.and(listOf(Query.greaterThan("age", 18), Query.lessThan("age", 65)))  // AND (default)
Query.relationship("comments", listOf(Query.equal("approved", true), Query.limit(25)))  // related documents

// Aggregations (optional alias)
Query.count("*", "total")
Query.sum("amount", "revenue")
Query.avg("score", "avgScore")
Query.min("price", "minPrice")
Query.max("price", "maxPrice")

// Grouping
Query.groupBy(listOf("status", "year"))
Query.having(listOf(Query.greaterThan("total", 1)))
Query.distinct()

// Joins (table id, left key, right key, operator, alias)
Query.join("orders", "\$id", "customerId")
Query.leftJoin("orders", "\$id", "customerId", "=", "ord")
Query.leftJoin("orders", "ord", listOf(
    Query.on("\$id", "customerId"),
    Query.equal("ord.status", "paid"),
))

// Fluent builder. Pass build() into list methods.
val queries = Query.builder()
    .equal("status", "published")
    .leftJoin("orders", "\$id", "customerId", "=", "ord")
    .count("*", "total")
    .groupBy(listOf("status"))
    .limit(25)
    .build()

tablesDB.listRows(
    databaseId = "[DATABASE_ID]",
    tableId = "[TABLE_ID]",
    queries = queries,
)
ファイルストレージ
val storage = Storage(client)

// Upload file
val file = storage.createFile(
    bucketId = "[BUCKET_ID]",
    fileId = ID.unique(),
    file = InputFile.fromPath("/path/to/file.png")
)

// Get file preview
val preview = storage.getFilePreview(
    bucketId = "[BUCKET_ID]",
    fileId = "[FILE_ID]",
    width = 300,
    height = 300
)

// List files
val files = storage.listFiles(bucketId = "[BUCKET_ID]")

// Delete file
storage.deleteFile(bucketId = "[BUCKET_ID]", fileId = "[FILE_ID]")
InputFile ファクトリメソッド
import io.appwrite.models.InputFile

InputFile.fromPath("/path/to/file.png")              // from filesystem path
InputFile.fromBytes(byteArray, "file.png")           // from ByteArray
チーム
val teams = Teams(client)

// Create team
val team = teams.create(teamId = ID.unique(), name = "Engineering")

// List teams
val list = teams.list()

// Create membership (invite user by email)
val membership = teams.createMembership(
    teamId = "[TEAM_ID]",
    roles = listOf("editor"),
    email = "[email protected]"
)

// List memberships
val members = teams.listMemberships(teamId = "[TEAM_ID]")

// Update membership roles
teams.updateMembership(teamId = "[TEAM_ID]", membershipId = "[MEMBERSHIP_ID]", roles = listOf("admin"))

// Delete team
teams.delete(teamId = "[TEAM_ID]")

ロールベースのアクセス: 権限を設定する際は、すべてのチームメンバーには Role.team("[TEAM_ID]") を、特定のチームロールには Role.team("[TEAM_ID]", "editor") を使用してください。

リアルタイムサブスクリプション(クライアントサイド)
import io.appwrite.Channel

val realtime = Realtime(client)

// Subscribe to row changes
val subscription = realtime.subscribe(
    Channel.tablesdb("[DATABASE_ID]").table("[TABLE_ID]").row()
) { response ->
    println(response.events)   // e.g. ["tablesdb.*.tables.*.rows.*.create"]
    println(response.payload)  // the affected resource
}

// Subscribe to multiple channels
val multi = realtime.subscribe(
    Channel.tablesdb("[DATABASE_ID]").table("[TABLE_ID]").row(),
    Channel.bucket("[BUCKET_ID]").file()
) { response -> /* ... */ }

// Cleanup
subscription.close()

利用可能なチャンネル:

チャンネル説明
account認証済みユーザーのアカウントへの変更
tablesdb.[DB_ID].tables.[TABLE_ID].rowsテーブル内のすべての行
tablesdb.[DB_ID].tables.[TABLE_ID].rows.[ROW_ID]特定の行
buckets.[BUCKET_ID].filesバケット内のすべてのファイル
buckets.[BUCKET_ID].files.[FILE_ID]特定のファイル
teamsユーザーが所属するチームへの変更
teams.[TEAM_ID]特定のチーム
membershipsユーザーのチームメンバーシップ
functions.[FUNCTION_ID].executions関数実行の更新

レスポンスフィールド: events(配列)、payload(リソース)、channels(一致したチャンネル)、timestamp(ISO 8601)。

サーバーレス関数(サーバーサイド)
val functions = Functions(client)

// Execute function
val execution = functions.createExecution(
    functionId = "[FUNCTION_ID]",
    body = """{"key": "value"}"""
)

// List executions
val executions = functions.listExecutions(functionId = "[FUNCTION_ID]")
関数ハンドラーの記述(Kotlin ランタイム)
// src/Main.kt — Appwrite Function entry point
import io.openruntimes.kotlin.RuntimeContext
import io.openruntimes.kotlin.RuntimeOutput

fun main(context: RuntimeContext): RuntimeOutput {
    // context.req.body        — raw body (String)
    // context.req.bodyJson    — parsed JSON (Map)
    // context.req.headers     — headers (Map)
    // context.req.method      — HTTP method
    // context.req.path        — URL path
    // context.req.query       — query params (Map)

    context.log("Processing: ${context.req.method} ${context.req.path}")

    if (context.req.method == "GET") {
        return context.res.json(mapOf("message" to "Hello from Appwrite Function!"))
    }

    return context.res.json(mapOf("success" to true))    // JSON
    // context.res.text("Hello")                          // plain text
    // context.res.empty()                                // 204
    // context.res.redirect("https://...")                 // 302
}
サーバーサイドレンダリング(SSR)認証

Kotlin サーバーフレームワーク(Ktor、Spring Boot など)を使用する SSR アプリは、認証を処理するために サーバー SDK を使用します。2 つのクライアントが必要です:

  • 管理者クライアント — API キーを使用し、セッションを作成し、レート制限をバイパスします(再利用可能なシングルトン)
  • セッションクライアント — セッション Cookie を使用し、ユーザーの代理として動作します(リクエストごとに作成し、決して共有しないでください)
import io.appwrite.Client
import io.appwrite.services.Account
import io.appwrite.enums.OAuthProvider

// Admin client (reusable)
val adminClient = Client()
    .setEndpoint("https://<REGION>.cloud.appwrite.io/v1")
    .setProject("[PROJECT_ID]")
    .setKey(System.getenv("APPWRITE_API_KEY"))

// Session client (create per-request)
val sessionClient = Client()
    .setEndpoint("https://<REGION>.cloud.appwrite.io/v1")
    .setProject("[PROJECT_ID]")

val session = call.request.cookies["a_session_[PROJECT_ID]"]
if (session != null) {
    sessionClient.setSession(session)
}
メール/パスワードログイン(Ktor)
post("/login") {
    val body = call.receive<LoginRequest>()
    val account = Account(adminClient)
    val session = account.createEmailPasswordSession(
        email = body.email,
        password = body.password,
    )

    // Cookie name must be a_session_<PROJECT_ID>
    call.response.cookies.append(Cookie(
        name = "a_session_[PROJECT_ID]",
        value = session.secret,
        httpOnly = true,
        secure = true,
        extensions = mapOf("SameSite" to "Strict"),
        path = "/",
    ))
    call.respond(mapOf("success" to true))
}
認証済みリクエスト
get("/user") {
    val session = call.request.cookies["a_session_[PROJECT_ID]"]
        ?: return@get call.respond(HttpStatusCode.Unauthorized)

    val sessionClient = Client()
        .setEndpoint("https://<REGION>.cloud.appwrite.io/v1")
        .setProject("[PROJECT_ID]")
        .setSession(session)

    val account = Account(sessionClient)
    val user = account.get()
    call.respond(user)
}
OAuth2 SSR フロー
// Step 1: Redirect to OAuth provider
get("/oauth") {
    val account = Account(adminClient)
    val redirectUrl = account.createOAuth2Token(
        provider = OAuthProvider.GITHUB,
        success = "https://example.com/oauth/success",
        failure = "https://example.com/oauth/failure",
    )
    call.respondRedirect(redirectUrl)
}

// Step 2: Handle callback — exchange token for session
get("/oauth/success") {
    val account = Account(adminClient)
    val session = account.createSession(
        userId = call.parameters["userId"]!!,
        secret = call.parameters["secret"]!!,
    )

    call.response.cookies.append(Cookie(
        name = "a_session_[PROJECT_ID]", value = session.secret,
        httpOnly = true, secure = true,
        extensions = mapOf("SameSite" to "Strict"), path = "/",
    ))
    call.respond(mapOf("success" to true))
}

Cookie のセキュリティ: XSS を防ぐために、常に httpOnly、secure、SameSite=Strict を使用してください。Cookie 名は a_session_<PROJECT_ID> である必要があります。

ユーザーエージェントの転送: デバッグとセキュリティのためにエンドユーザーのブラウザ情報を記録するには、sessionClient.setForwardedUserAgent(call.request.headers["User-Agent"]) を呼び出してください。

エラーハンドリング

import io.appwrite.AppwriteException

try {
    val row = tablesDB.getRow(databaseId = "[DATABASE_ID]", tableId = "[TABLE_ID]", rowId = "[ROW_ID]")
} catch (e: AppwriteException) {
    println(e.message)     // human-readable message
    println(e.code)        // HTTP status code (Int)
    println(e.type)        // error type (e.g. "document_not_found")
    println(e.response)    // full response body (Map)
}

一般的なエラーコード:

コード意味
401認証されていません — セッション/API キーが欠落しているか無効です
403禁止されています — 権限が不十分です
404見つかりません — リソースが存在しません
409競合 — ID の重複または一意制約違反
429レート制限 — リクエストが多すぎます

権限とロール (重要)

Appwrite は権限文字列を使用してリソースへのアクセスを制御します。各権限は、アクション (read、update、delete、create、または create + update + delete を許可する write) とロールターゲットを組み合わせたものです。デフォルトでは、行/ファイルレベルで権限を明示的に設定するか、テーブル/バケットの設定から継承しない限り、どのユーザーもアクセスできません。権限は Permission および Role ヘルパーで構築された文字列の配列です。

import io.appwrite.Permission
import io.appwrite.Role
権限付きのデータベース行
val doc = tablesDB.createRow(
    databaseId = "[DATABASE_ID]",
    tableId = "[TABLE_ID]",
    rowId = ID.unique(),
    data = mapOf("title" to "Hello World"),
    permissions = listOf(
        Permission.read(Role.user("[USER_ID]")),     // specific user can read
        Permission.update(Role.user("[USER_ID]")),   // specific user can update
        Permission.read(Role.team("[TEAM_ID]")),     // all team members can read
        Permission.read(Role.any()),                 // anyone (including guests) can read
    )
)
権限付きのファイルアップロード
val file = storage.createFile(
    bucketId = "[BUCKET_ID]",
    fileId = ID.unique(),
    file = InputFile.fromPath("/path/to/file.png"),
    permissions = listOf(
        Permission.read(Role.any()),
        Permission.update(Role.user("[USER_ID]")),
        Permission.delete(Role.user("[USER_ID]")),
    )
)

権限を設定するタイミング: リソースごとのアクセス制御が必要な場合は、行/ファイルレベルの権限を設定します。テーブル内のすべての行が同じルールを共有する場合は、テーブル/バケットレベルで権限を設定し、行の権限は空のままにします。

よくある間違い:

  • 権限の設定忘れ — リソースがすべてのユーザー (作成者を含む) からアクセスできなくなります
  • write/update/delete と Role.any() の併用 — 未認証のゲストを含む任意のユーザーがリソースを変更または削除できるようになります
  • 機密データに対する Permission.read(Role.any()) — リソースが公開で読み取り可能になります

コメント