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. 05调用 Appwrite 云函数

适用场景

  1. 01为 Apple 平台应用添加 Appwrite 认证
  2. 02在 Swift 服务端管理用户与数据
  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]")
数据库操作

注意: 所有新代码都应使用 TablesDB(而非已弃用的 Databases 类)。仅当现有代码库已依赖 Databases,或用户明确要求时,才使用 Databases。

提示: 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]")
字符串列类型

注意: 旧版 string 类型已弃用。所有新列都应使用明确的列类型。

类型最大字符数索引存储
varchar16,383完整索引(如果大小 ≤ 768)行内
text16,383仅前缀页外
mediumtext4,194,303仅前缀页外
longtext1,073,741,823仅前缀页外
  • varchar 存储在行内,并计入 64 KB 的行大小限制。适用于名称、slug 或标识符等较短且需要索引的字段。
  • 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处理身份验证。你需要两个客户端:

  • 管理客户端——使用 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 安全: 始终使用 isHTTPOnly、isSecure 和 sameSite: .strict 来防止 XSS。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]")),
    ]
)

何时设置权限: 当需要对每个资源进行访问控制时,请设置行/文件级权限。如果表中的所有行都使用相同的规则,请在表/存储桶级别配置权限,并将行权限留空。

常见错误:

  • 忘记设置权限 — 资源将对所有用户(包括创建者)不可访问
  • 对 Role.any() 使用 write/update/delete — 允许任何用户(包括未通过身份验证的访客)修改或删除资源
  • 在敏感数据上使用 Permission.read(Role.any()) — 使资源可被公开读取

评论

支持 Markdown