mcpField
A
appwrite-dart

appwrite-dart

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

appwrite-dart について

appwrite-dartは、FlutterクライアントとDartサーバー向けのAppwrite SDKです。認証、TablesDB、ファイルストレージ、チーム、リアルタイム購読、関数のAPIを提供し、権限とロールも設定できます。

主な機能

  1. 01Appwriteプロジェクトへの接続
  2. 02認証とユーザー管理
  3. 03TablesDBとクエリの操作
  4. 04ファイルのアップロードと権限設定
  5. 05チームとロールの管理
  6. 06関数の実行とリアルタイムイベントの購読

活用シーン

  1. 01FlutterアプリでAppwriteサービスを利用
  2. 02Dartサーバーからユーザーや関数を管理
  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]');
データベース操作

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

Tip: すべての 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]',
);
文字列カラム型

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
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)を使用します。2 つのクライアントが必要です:

  • 管理クライアント — 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 セキュリティ: XSS を防ぐために、常に HttpOnly、Secure、SameSite=Strict を使用してください。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]')),
    ],
);

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

よくある間違い:

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

コメント