mcpField
A
appwrite-dart

appwrite-dart

github.com/appwrite开发281.1k 次安装1 周前更新
代码仓库

关于 appwrite-dart

appwrite-dart 是面向 Flutter 客户端和 Dart 服务端的 Appwrite SDK。它提供身份验证、TablesDB、文件存储、团队、实时订阅及函数等 API,并支持权限与角色控制。

主要功能

  1. 01连接 Appwrite 项目
  2. 02处理身份验证与用户管理
  3. 03操作 TablesDB 和查询
  4. 04上传文件并设置权限
  5. 05管理团队与角色
  6. 06执行函数并订阅实时事件

适用场景

  1. 01在 Flutter 应用中接入 Appwrite 服务
  2. 02通过 Dart 服务端管理用户和函数
  3. 03为表行及文件配置访问权限
  4. 04订阅账户、表格或团队变更

安装

复制下面的命令,在终端中运行。

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

安装后在 Claude Code 中输入/appwrite-dart确认已加载。

详细介绍

Appwrite Dart SDK

安装

# Flutter (client-side)
flutter pub add appwrite

# Dart (server-side)
dart pub add dart_appwrite

设置客户端

客户端(Flutter)
import 'package:appwrite/appwrite.dart';

final client = Client()
    .setEndpoint('https://<REGION>.cloud.appwrite.io/v1')
    .setProject('[PROJECT_ID]');
服务端(Dart)
import 'package:dart_appwrite/dart_appwrite.dart';

final client = Client()
    .setEndpoint('https://<REGION>.cloud.appwrite.io/v1')
    .setProject(Platform.environment['APPWRITE_PROJECT_ID']!)
    .setKey(Platform.environment['APPWRITE_API_KEY']!);

代码示例

身份验证(客户端)
final account = Account(client);

// Signup
await account.create(userId: ID.unique(), email: '[email protected]', password: 'password123', name: 'User Name');

// Login
final session = await account.createEmailPasswordSession(email: '[email protected]', password: 'password123');

// OAuth login
await account.createOAuth2Session(provider: OAuthProvider.google);

// Get current user
final user = await account.get();

// Logout
await account.deleteSession(sessionId: 'current');
用户管理(服务端)
final users = Users(client);

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

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

// Get user
final fetched = await users.get(userId: '[USER_ID]');

// Delete user
await users.delete(userId: '[USER_ID]');
数据库操作

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

提示: SDK 方法调用应优先使用命名参数(例如 databaseId: '...')。仅当现有代码库已使用位置参数,或用户明确要求时,才使用位置参数。

final tablesDB = TablesDB(client);

// Create database (server-side only)
final db = await tablesDB.create(databaseId: ID.unique(), name: 'My Database');

// Create table (server-side only)
final col = await tablesDB.createTable(databaseId: '[DATABASE_ID]', tableId: ID.unique(), name: 'My Table');

// Create row
final doc = await tablesDB.createRow(
    databaseId: '[DATABASE_ID]',
    tableId: '[TABLE_ID]',
    rowId: ID.unique(),
    data: {'title': 'Hello', 'done': false},
);

// Query rows
final results = await tablesDB.listRows(
    databaseId: '[DATABASE_ID]',
    tableId: '[TABLE_ID]',
    queries: [Query.equal('done', false), Query.limit(10)],
);

// Get row
final row = await tablesDB.getRow(databaseId: '[DATABASE_ID]', tableId: '[TABLE_ID]', rowId: '[ROW_ID]');

// Update row
await tablesDB.updateRow(
    databaseId: '[DATABASE_ID]',
    tableId: '[TABLE_ID]',
    rowId: '[ROW_ID]',
    data: {'done': true},
);

// Delete row
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
await tablesDB.createTable(
    databaseId: '[DATABASE_ID]',
    tableId: ID.unique(),
    name: 'articles',
    columns: [
        {'key': 'title',    'type': 'varchar',    'size': 255, 'required': true},   // inline, fully indexable
        {'key': 'summary',  'type': 'text',                    'required': false},  // off-page, prefix index only
        {'key': 'body',     'type': 'mediumtext',              'required': false},  // up to ~4 M chars
        {'key': 'raw_data', 'type': 'longtext',                'required': false},  // up to ~1 B chars
    ],
);
查询方法
// 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(['field1', 'field2'])        // return only specified fields
Query.or([Query.equal('a', 1), Query.equal('b', 2)])   // OR
Query.and([Query.greaterThan('age', 18), Query.lessThan('age', 65)])  // AND (default)
Query.relationship('comments', [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(['status', 'year'])
Query.having([Query.greaterThan('total', 1)])
Query.distinct()

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

// Fluent builder. Pass build() into list methods.
final queries = Query.builder()
    .equal('status', 'published')
    .leftJoin('orders', r'$id', 'customerId', '=', 'ord')
    .count('*', 'total')
    .groupBy(['status'])
    .limit(25)
    .build();

await tablesDB.listRows(
    databaseId: '[DATABASE_ID]',
    tableId: '[TABLE_ID]',
    queries: queries,
);
文件存储
final storage = Storage(client);

// Upload file
final file = await storage.createFile(
    bucketId: '[BUCKET_ID]',
    fileId: ID.unique(),
    file: InputFile.fromPath(path: '/path/to/file.png', filename: 'file.png'),
);

// Get file preview
final preview = storage.getFilePreview(bucketId: '[BUCKET_ID]', fileId: '[FILE_ID]', width: 300, height: 300);

// List files
final files = await storage.listFiles(bucketId: '[BUCKET_ID]');

// Delete file
await storage.deleteFile(bucketId: '[BUCKET_ID]', fileId: '[FILE_ID]');
InputFile 工厂方法
// Client-side (Flutter)
InputFile.fromPath(path: '/path/to/file.png', filename: 'file.png')    // from path
InputFile.fromBytes(bytes: uint8List, filename: 'file.png')            // from Uint8List

// Server-side (Dart)
InputFile.fromPath(path: '/path/to/file.png', filename: 'file.png')
InputFile.fromBytes(bytes: uint8List, filename: 'file.png')
团队
final teams = Teams(client);

// Create team
final team = await teams.create(teamId: ID.unique(), name: 'Engineering');

// List teams
final list = await teams.list();

// Create membership (invite user by email)
final membership = await teams.createMembership(
    teamId: '[TEAM_ID]',
    roles: ['editor'],
    email: '[email protected]',
);

// List memberships
final members = await teams.listMemberships(teamId: '[TEAM_ID]');

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

// Delete team
await teams.delete(teamId: '[TEAM_ID]');

基于角色的访问: 设置权限时,使用 Role.team('[TEAM_ID]') 表示所有团队成员,或使用 Role.team('[TEAM_ID]', 'editor') 指定特定团队角色。

实时订阅(客户端)
final realtime = Realtime(client);

// Subscribe to row changes
final subscription = realtime.subscribe([
    Channel.tablesdb('[DATABASE_ID]').table('[TABLE_ID]').row(),
]);
subscription.stream.listen((response) {
    print(response.events);   // e.g. ['tablesdb.*.tables.*.rows.*.create']
    print(response.payload);  // the affected resource
});

// Subscribe to multiple channels
final multi = realtime.subscribe([
    Channel.tablesdb('[DATABASE_ID]').table('[TABLE_ID]').row(),
    Channel.bucket('[BUCKET_ID]').file(),
]);

// 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用户的团队成员关系
memberships.[MEMBERSHIP_ID]特定成员关系
functions.[FUNCTION_ID].executions函数执行更新

响应字段:events(数组)、payload(资源)、channels(匹配的频道)、timestamp(ISO 8601)。

无服务器函数(服务端)
final functions = Functions(client);

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

// List executions
final executions = await functions.listExecutions(functionId: '[FUNCTION_ID]');
编写函数处理程序(Dart 运行时)
// lib/main.dart — Appwrite Function entry point
Future<dynamic> main(final context) async {
    // context.req.body        — raw body (String)
    // context.req.bodyJson    — parsed JSON (Map or null)
    // 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({'message': 'Hello from Appwrite Function!'});
    }

    return context.res.json({'success': true});      // JSON
    // return context.res.text('Hello');              // plain text
    // return context.res.empty();                    // 204
    // return context.res.redirect('https://...');    // 302
}
服务端渲染(SSR)身份验证

使用服务端 Dart(Dart Frog、Shelf 等)的 SSR 应用会使用服务端 SDK(dart_appwrite)处理身份验证。你需要两个客户端:

  • 管理员客户端 — 使用 API 密钥创建会话,并绕过速率限制(可复用的单例)
  • 会话客户端 — 使用会话 Cookie 代表用户执行操作(每个请求创建一个,切勿共享)
import 'package:dart_appwrite/dart_appwrite.dart';

// Admin client (reusable)
final adminClient = Client()
    .setEndpoint('https://<REGION>.cloud.appwrite.io/v1')
    .setProject('[PROJECT_ID]')
    .setKey(Platform.environment['APPWRITE_API_KEY']!);

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

final session = request.cookies['a_session_[PROJECT_ID]'];
if (session != null) {
    sessionClient.setSession(session);
}
电子邮件/密码登录
final account = Account(adminClient);
final session = await account.createEmailPasswordSession(
    email: body['email'],
    password: body['password'],
);

// Cookie name must be a_session_<PROJECT_ID>
response.headers.add('Set-Cookie',
    'a_session_[PROJECT_ID]=${session.secret}; '
    'HttpOnly; Secure; SameSite=Strict; '
    'Expires=${HttpDate.format(DateTime.parse(session.expire))}; Path=/');
已验证请求
final session = request.cookies['a_session_[PROJECT_ID]'];
if (session == null) {
    return Response(statusCode: 401, body: 'Unauthorized');
}

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

final account = Account(sessionClient);
final user = await account.get();
OAuth2 SSR 流程
// Step 1: Redirect to OAuth provider
final account = Account(adminClient);
final redirectUrl = await account.createOAuth2Token(
    provider: OAuthProvider.github,
    success: 'https://example.com/oauth/success',
    failure: 'https://example.com/oauth/failure',
);
return Response(statusCode: 302, headers: {'Location': redirectUrl});

// Step 2: Handle callback — exchange token for session
final account = Account(adminClient);
final session = await account.createSession(
    userId: request.uri.queryParameters['userId']!,
    secret: request.uri.queryParameters['secret']!,
);
// Set session cookie as above

Cookie 安全: 始终使用 HttpOnly、Secure 和 SameSite=Strict,以防止 XSS。Cookie 名称必须为 a_session_<PROJECT_ID>。

转发用户代理: 调用 sessionClient.setForwardedUserAgent(request.headers['user-agent']),记录最终用户的浏览器信息,以便调试和保障安全。

错误处理

import 'package:appwrite/appwrite.dart';
// AppwriteException is included in the main import

try {
    final row = await tablesDB.getRow(databaseId: '[DATABASE_ID]', tableId: '[TABLE_ID]', rowId: '[ROW_ID]');
} on AppwriteException catch (e) {
    print(e.message);    // human-readable message
    print(e.code);       // HTTP status code (int)
    print(e.type);       // error type (e.g. 'document_not_found')
    print(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 'package:appwrite/appwrite.dart';
// Permission and Role are included in the main package import
带权限的数据库行
final doc = 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
    ],
);
带权限的文件上传
final file = await storage.createFile(
    bucketId: '[BUCKET_ID]',
    fileId: ID.unique(),
    file: InputFile.fromPath(path: '/path/to/file.png', filename: '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