mcpField
A
appwrite-swift

appwrite-swift

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

appwrite-swift について

appwrite-swift は、Apple プラットフォームと Swift サーバーアプリ向けの Appwrite SDK です。認証、TablesDB、ファイルストレージ、リアルタイム購読、関数実行に対応し、SSR 認証の例も含みます。

主な機能

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

活用シーン

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

インストール

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

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

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

詳細

Appwrite Swift SDK

インストール

// Swift Package Manager — Package.swift
.package(url: "https://github.com/appwrite/sdk-for-swift", branch: "main")

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

クライアントサイド (Apple プラットフォーム)
import Appwrite

let client = Client()
    .setEndpoint("https://<REGION>.cloud.appwrite.io/v1")
    .setProject("[PROJECT_ID]")
サーバーサイド (Swift)
import Appwrite

let client = Client()
    .setEndpoint("https://<REGION>.cloud.appwrite.io/v1")
    .setProject(ProcessInfo.processInfo.environment["APPWRITE_PROJECT_ID"]!)
    .setKey(ProcessInfo.processInfo.environment["APPWRITE_API_KEY"]!)

コード例

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

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

// Login
let session = try await account.createEmailPasswordSession(email: "[email protected]", password: "password123")

// OAuth
try await account.createOAuth2Session(provider: .google)

// Get current user
let me = try await account.get()

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

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

// List users
let list = try await users.list(queries: [Query.limit(25)])

// Get user
let fetched = try await users.get(userId: "[USER_ID]")

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

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

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

let tablesDB = TablesDB(client)

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

// Create row
let doc = try await tablesDB.createRow(databaseId: "[DATABASE_ID]", tableId: "[TABLE_ID]", rowId: ID.unique(), data: [
    "title": "Hello",
    "done": false
])

// Query rows
let results = try await tablesDB.listRows(databaseId: "[DATABASE_ID]", tableId: "[TABLE_ID]", queries: [
    Query.equal("done", value: false),
    Query.limit(10)
])

// Get row
let row = try await tablesDB.getRow(databaseId: "[DATABASE_ID]", tableId: "[TABLE_ID]", rowId: "[ROW_ID]")

// Update row
try await tablesDB.updateRow(databaseId: "[DATABASE_ID]", tableId: "[TABLE_ID]", rowId: "[ROW_ID]", data: ["done": true])

// Delete row
try await 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
try await tablesDB.createTable(
    databaseId: "[DATABASE_ID]",
    tableId: ID.unique(),
    name: "articles",
    columns: [
        ["key": "title",    "type": "varchar",    "size": 255, "required": true],
        ["key": "summary",  "type": "text",                    "required": false],
        ["key": "body",     "type": "mediumtext",              "required": false],
        ["key": "raw_data", "type": "longtext",                "required": false],
    ]
)
クエリメソッド
// Filtering
Query.equal("field", value: "value")          // == (or pass array for IN)
Query.notEqual("field", value: "value")       // !=
Query.lessThan("field", value: 100)           // <
Query.lessThanEqual("field", value: 100)      // <=
Query.greaterThan("field", value: 100)        // >
Query.greaterThanEqual("field", value: 100)   // >=
Query.between("field", start: 1, end: 100)    // 1 <= field <= 100
Query.isNull("field")                         // is null
Query.isNotNull("field")                      // is not null
Query.startsWith("field", value: "prefix")    // starts with
Query.endsWith("field", value: "suffix")      // ends with
Query.contains("field", value: "sub")         // contains
Query.search("field", value: "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(["field1", "field2"])
Query.or([Query.equal("a", value: 1), Query.equal("b", value: 2)])   // OR
Query.and([Query.greaterThan("age", value: 18), Query.lessThan("age", value: 65)])  // AND (default)
Query.relationship("comments", queries: [Query.equal("approved", value: true), Query.limit(25)])  // related documents

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

// Grouping
Query.groupBy(["status", "year"])
Query.having([Query.greaterThan("total", value: 1)])
Query.distinct()

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

// Fluent builder. Pass build() into list methods.
let queries = Query.builder()
    .equal("status", value: "published")
    .leftJoin("orders", left: "$id", right: "customerId", operator: "=", alias: "ord")
    .count("*", alias: "total")
    .groupBy(["status"])
    .limit(25)
    .build()

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

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

// List files
let files = try await storage.listFiles(bucketId: "[BUCKET_ID]")

// Delete file
try await storage.deleteFile(bucketId: "[BUCKET_ID]", fileId: "[FILE_ID]")
InputFile ファクトリメソッド
InputFile.fromPath("/path/to/file.png")                    // from filesystem path
InputFile.fromData(data, filename: "file.png", mimeType: "image/png")  // from Data
チーム
let teams = Teams(client)

// Create team
let team = try await teams.create(teamId: ID.unique(), name: "Engineering")

// List teams
let list = try await teams.list()

// Create membership (invite user by email)
let membership = try await teams.createMembership(
    teamId: "[TEAM_ID]",
    roles: ["editor"],
    email: "[email protected]"
)

// List memberships
let members = try await teams.listMemberships(teamId: "[TEAM_ID]")

// Update membership roles
try await teams.updateMembership(teamId: "[TEAM_ID]", membershipId: "[MEMBERSHIP_ID]", roles: ["admin"])

// Delete team
try await teams.delete(teamId: "[TEAM_ID]")

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

リアルタイムサブスクリプション (クライアントサイド)
let realtime = Realtime(client)

// Subscribe to row changes
let subscription = try await realtime.subscribe(channels: [
    Channel.tablesdb("[DATABASE_ID]").table("[TABLE_ID]").row()
]) { response in
    print(response.events)   // e.g. ["tablesdb.*.tables.*.rows.*.create"]
    print(response.payload)  // the affected resource
}

// Subscribe to multiple channels
let multi = try await realtime.subscribe(channels: [
    Channel.tablesdb("[DATABASE_ID]").table("[TABLE_ID]").row(),
    Channel.files(),
]) { response in /* ... */ }

// Cleanup
try await 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)。

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

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

// List executions
let executions = try await functions.listExecutions(functionId: "[FUNCTION_ID]")
関数ハンドラーの記述 (Swift ランタイム)
// Sources/main.swift — Appwrite Function entry point
func main(context: RuntimeContext) async throws -> RuntimeOutput {
    // context.req.body        — raw body (String)
    // context.req.bodyJson    — parsed JSON ([String: Any]?)
    // context.req.headers     — headers ([String: String])
    // context.req.method      — HTTP method
    // context.req.path        — URL path
    // context.req.query       — query params ([String: String])

    context.log("Processing: \(context.req.method) \(context.req.path)")

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

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

サーバーサイド Swift (Vapor、Hummingbird など) を使用する SSR アプリは、認証を処理するために サーバー SDK を使用します。2 つのクライアントが必要です:

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

// Admin client (reusable)
let adminClient = Client()
    .setEndpoint("https://<REGION>.cloud.appwrite.io/v1")
    .setProject("[PROJECT_ID]")
    .setKey(Environment.get("APPWRITE_API_KEY")!)

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

if let session = req.cookies["a_session_[PROJECT_ID]"]?.string {
    sessionClient.setSession(session)
}
メール/パスワードログイン (Vapor)
app.post("login") { req async throws -> Response in
    let body = try req.content.decode(LoginRequest.self)
    let account = Account(adminClient)
    let session = try await account.createEmailPasswordSession(
        email: body.email,
        password: body.password
    )

    // Cookie name must be a_session_<PROJECT_ID>
    let response = Response(status: .ok, body: .init(string: "{\"success\": true}"))
    response.cookies["a_session_[PROJECT_ID]"] = HTTPCookies.Value(
        string: session.secret,
        isHTTPOnly: true,
        isSecure: true,
        sameSite: .strict,
        path: "/"
    )
    return response
}
認証済みリクエスト
app.get("user") { req async throws -> Response in
    guard let session = req.cookies["a_session_[PROJECT_ID]"]?.string else {
        throw Abort(.unauthorized)
    }

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

    let account = Account(sessionClient)
    let user = try await account.get()
    // Return user as JSON
}
OAuth2 SSR フロー
// Step 1: Redirect to OAuth provider
app.get("oauth") { req async throws -> Response in
    let account = Account(adminClient)
    let redirectUrl = try await account.createOAuth2Token(
        provider: .github,
        success: "https://example.com/oauth/success",
        failure: "https://example.com/oauth/failure"
    )
    return req.redirect(to: redirectUrl)
}

// Step 2: Handle callback — exchange token for session
app.get("oauth", "success") { req async throws -> Response in
    let userId = try req.query.get(String.self, at: "userId")
    let secret = try req.query.get(String.self, at: "secret")

    let account = Account(adminClient)
    let session = try await account.createSession(userId: userId, secret: secret)

    let response = Response(status: .ok, body: .init(string: "{\"success\": true}"))
    response.cookies["a_session_[PROJECT_ID]"] = HTTPCookies.Value(
        string: session.secret,
        isHTTPOnly: true, isSecure: true, sameSite: .strict, path: "/"
    )
    return response
}

Cookie のセキュリティ: XSS を防ぐために、常に isHTTPOnly、isSecure、sameSite: .strict を使用してください。Cookie 名は a_session_<PROJECT_ID> でなければなりません。

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

エラーハンドリング

import Appwrite
// AppwriteException is included in the main module

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

一般的なエラーコード:

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

権限とロール (重要)

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

import Appwrite
// Permission and Role are included in the main module import
権限付きのデータベース行
let doc = try await tablesDB.createRow(
    databaseId: "[DATABASE_ID]",
    tableId: "[TABLE_ID]",
    rowId: ID.unique(),
    data: ["title": "Hello World"],
    permissions: [
        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
    ]
)
権限付きのファイルアップロード
let file = try await storage.createFile(
    bucketId: "[BUCKET_ID]",
    fileId: ID.unique(),
    file: InputFile.fromPath("/path/to/file.png"),
    permissions: [
        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()) — リソースが公開で読み取り可能になります

コメント