appwrite-dart
Appwrite の Dart SDK に関するスキルです。Flutter アプリ(モバイル・Web・デスクトップ)またはサーバーサイド Dart アプリと Appwrite を連携する際に使用します。メール・OAuth 認証、データベースクエリ、ネイティブファイル操作によるアップロード、リアルタイムサブスクリプション、さらに API キーを用いたユーザー管理・データベース管理・ストレージ・Functions などのサーバーサイド管理機能まで幅広くカバーします。
description の原文を見る
Appwrite Dart SDK skill. Use when building Flutter apps (mobile, web, desktop) or server-side Dart applications with Appwrite. Covers client-side auth (email, OAuth), database queries, file uploads with native file handling, real-time subscriptions, and server-side admin via API keys for user management, database administration, storage, and functions.
SKILL.md 本文
Appwrite Dart SDK
インストール
# Flutter (クライアント側)
flutter pub add appwrite
# Dart (サーバー側)
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);
// サインアップ
await account.create(userId: ID.unique(), email: 'user@example.com', password: 'password123', name: 'User Name');
// ログイン
final session = await account.createEmailPasswordSession(email: 'user@example.com', password: 'password123');
// OAuth ログイン
await account.createOAuth2Session(provider: OAuthProvider.google);
// 現在のユーザーを取得
final user = await account.get();
// ログアウト
await account.deleteSession(sessionId: 'current');
ユーザー管理 (サーバー側)
final users = Users(client);
// ユーザーを作成
final user = await users.create(userId: ID.unique(), email: 'user@example.com', password: 'password123', name: 'User Name');
// ユーザーをリスト表示
final list = await users.list(queries: [Query.limit(25)]);
// ユーザーを取得
final fetched = await users.get(userId: '[USER_ID]');
// ユーザーを削除
await users.delete(userId: '[USER_ID]');
データベース操作
注: すべての新規コードに
TablesDB(廃止されたDatabasesクラスではなく) を使用してください。既存のコードベースが既にDatabasesに依存している場合、またはユーザーが明示的にリクエストした場合にのみ使用してください。ヒント: すべての SDK メソッド呼び出しで名前付きパラメータ (例:
databaseId: '...') を推奨します。既存のコードベースで既に位置引数を使用している場合、またはユーザーが明示的にリクエストした場合のみ位置引数を使用してください。
final tablesDB = TablesDB(client);
// データベースを作成 (サーバー側のみ)
final db = await tablesDB.create(databaseId: ID.unique(), name: 'My Database');
// テーブルを作成 (サーバー側のみ)
final col = await tablesDB.createTable(databaseId: '[DATABASE_ID]', tableId: ID.unique(), name: 'My Table');
// 行を作成
final doc = await tablesDB.createRow(
databaseId: '[DATABASE_ID]',
tableId: '[TABLE_ID]',
rowId: ID.unique(),
data: {'title': 'Hello', 'done': false},
);
// 行をクエリ
final results = await tablesDB.listRows(
databaseId: '[DATABASE_ID]',
tableId: '[TABLE_ID]',
queries: [Query.equal('done', false), Query.limit(10)],
);
// 行を取得
final row = await tablesDB.getRow(databaseId: '[DATABASE_ID]', tableId: '[TABLE_ID]', rowId: '[ROW_ID]');
// 行を更新
await tablesDB.updateRow(
databaseId: '[DATABASE_ID]',
tableId: '[TABLE_ID]',
rowId: '[ROW_ID]',
data: {'done': true},
);
// 行を削除
await tablesDB.deleteRow(
databaseId: '[DATABASE_ID]',
tableId: '[TABLE_ID]',
rowId: '[ROW_ID]',
);
文字列カラム型
注: 廃止予定の
string型は使用できません。すべての新規カラムに明示的なカラム型を使用してください。
| 型 | 最大文字数 | インデックス | ストレージ |
|---|---|---|---|
varchar | 16,383 | 完全インデックス (サイズ ≤ 768) | 行内にインライン化 |
text | 16,383 | プレフィックスのみ | ページ外 |
mediumtext | 4,194,303 | プレフィックスのみ | ページ外 |
longtext | 1,073,741,823 | プレフィックスのみ | ページ外 |
varcharはインラインで保存され、64 KB の行サイズ制限にカウントされます。名前、スラッグ、識別子など、短いインデックス付きフィールドに推奨されます。text、mediumtext、longtextはページ外に保存されます (20 バイトのポインタのみが行に存在するため)。行のサイズ予算を消費しません。これらの型にはsizeは不要です。
// 明示的な文字列カラム型でテーブルを作成
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}, // 最大 ~4M 文字
{'key': 'raw_data', 'type': 'longtext', 'required': false}, // 最大 ~1B 文字
],
);
クエリメソッド
// フィルタリング
Query.equal('field', 'value') // == (またはリストを渡して 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') // フルテキスト検索 (インデックスが必要)
// ソート
Query.orderAsc('field')
Query.orderDesc('field')
// ページネーション
Query.limit(25) // 最大行数 (デフォルト 25、最大 100)
Query.offset(0) // N 行スキップ
Query.cursorAfter('[ROW_ID]') // カーソルページネーション (推奨)
Query.cursorBefore('[ROW_ID]')
// 選択とロジック
Query.select(['field1', 'field2']) // 指定されたフィールドのみを返す
Query.or([Query.equal('a', 1), Query.equal('b', 2)]) // OR
Query.and([Query.greaterThan('age', 18), Query.lessThan('age', 65)]) // AND (デフォルト)
ファイルストレージ
final storage = Storage(client);
// ファイルをアップロード
final file = await storage.createFile(
bucketId: '[BUCKET_ID]',
fileId: ID.unique(),
file: InputFile.fromPath(path: '/path/to/file.png', filename: 'file.png'),
);
// ファイルプレビューを取得
final preview = storage.getFilePreview(bucketId: '[BUCKET_ID]', fileId: '[FILE_ID]', width: 300, height: 300);
// ファイルをリスト表示
final files = await storage.listFiles(bucketId: '[BUCKET_ID]');
// ファイルを削除
await storage.deleteFile(bucketId: '[BUCKET_ID]', fileId: '[FILE_ID]');
InputFile ファクトリメソッド
// クライアント側 (Flutter)
InputFile.fromPath(path: '/path/to/file.png', filename: 'file.png') // パスから
InputFile.fromBytes(bytes: uint8List, filename: 'file.png') // Uint8List から
// サーバー側 (Dart)
InputFile.fromPath(path: '/path/to/file.png', filename: 'file.png')
InputFile.fromBytes(bytes: uint8List, filename: 'file.png')
チーム
final teams = Teams(client);
// チームを作成
final team = await teams.create(teamId: ID.unique(), name: 'Engineering');
// チームをリスト表示
final list = await teams.list();
// メンバーシップを作成 (メールでユーザーを招待)
final membership = await teams.createMembership(
teamId: '[TEAM_ID]',
roles: ['editor'],
email: 'user@example.com',
);
// メンバーシップをリスト表示
final members = await teams.listMemberships(teamId: '[TEAM_ID]');
// メンバーシップのロールを更新
await teams.updateMembership(teamId: '[TEAM_ID]', membershipId: '[MEMBERSHIP_ID]', roles: ['admin']);
// チームを削除
await teams.delete(teamId: '[TEAM_ID]');
ロールベースアクセス: すべてのチームメンバーに対して
Role.team('[TEAM_ID]')を使用するか、特定のチームロールに対してRole.team('[TEAM_ID]', 'editor')を使用してパーミッションを設定してください。
リアルタイムサブスクリプション (クライアント側)
final realtime = Realtime(client);
// 行の変更をサブスクライブ
final subscription = realtime.subscribe([
Channel.tablesdb('[DATABASE_ID]').table('[TABLE_ID]').row(),
]);
subscription.stream.listen((response) {
print(response.events); // 例: ['tablesdb.*.tables.*.rows.*.create']
print(response.payload); // 影響を受けたリソース
});
// 複数のチャネルをサブスクライブ
final multi = realtime.subscribe([
Channel.tablesdb('[DATABASE_ID]').table('[TABLE_ID]').row(),
Channel.bucket('[BUCKET_ID]').file(),
]);
// クリーンアップ
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);
// 関数を実行
final execution = await functions.createExecution(functionId: '[FUNCTION_ID]', body: '{"key": "value"}');
// 実行をリスト表示
final executions = await functions.listExecutions(functionId: '[FUNCTION_ID]');
関数ハンドラーの作成 (Dart ランタイム)
// lib/main.dart — Appwrite Function エントリーポイント
Future<dynamic> main(final context) async {
// context.req.body — 生のボディ (String)
// context.req.bodyJson — 解析された JSON (Map または null)
// context.req.headers — ヘッダー (Map)
// context.req.method — HTTP メソッド
// context.req.path — URL パス
// context.req.query — クエリパラメータ (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'); // プレーンテキスト
// 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';
// 管理クライアント (再利用可能)
final adminClient = Client()
.setEndpoint('https://<REGION>.cloud.appwrite.io/v1')
.setProject('[PROJECT_ID]')
.setKey(Platform.environment['APPWRITE_API_KEY']!);
// セッションクライアント (リクエストごとに作成)
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 名は 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 フロー
// ステップ 1: OAuth プロバイダーにリダイレクト
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});
// ステップ 2: コールバック処理 — トークンをセッションに交換
final account = Account(adminClient);
final session = await account.createSession(
userId: request.uri.queryParameters['userId']!,
secret: request.uri.queryParameters['secret']!,
);
// セッション Cookie を上記のように設定
Cookie セキュリティ: XSS を防ぐために常に
HttpOnly、Secure、SameSite=Strictを使用してください。Cookie 名はa_session_<PROJECT_ID>である必要があります。
ユーザーエージェントの転送:
sessionClient.setForwardedUserAgent(request.headers['user-agent'])を呼び出して、デバッグとセキュリティのためにエンドユーザーのブラウザ情報を記録してください。
エラーハンドリング
import 'package:appwrite/appwrite.dart';
// AppwriteException はメインのインポートに含まれています
try {
final row = await tablesDB.getRow(databaseId: '[DATABASE_ID]', tableId: '[TABLE_ID]', rowId: '[ROW_ID]');
} on AppwriteException catch (e) {
print(e.message); // 人間が読める形式のメッセージ
print(e.code); // HTTP ステータスコード (int)
print(e.type); // エラータイプ (例: 'document_not_found')
print(e.response); // 完全なレスポンスボディ (Map)
}
一般的なエラーコード:
| コード | 意味 |
|---|---|
401 | 無許可 — セッション/API キーがないまたは無効 |
403 | 禁止 — 権限不足 |
404 | 見つかりません — リソースが存在しません |
409 | 競合 — ID の重複またはユニーク制約違反 |
429 | レート制限 — リクエストが多すぎます |
パーミッションとロール (重要)
Appwrite はパーミッション文字列を使用してリソースへのアクセスを制御します。各パーミッションはアクション (read、update、delete、create、または write (create + update + delete を付与)) をロール対象と組み合わせます。デフォルトでは、パーミッションが行/ファイルレベルで明示的に設定されるか、テーブル/バケット設定から継承されない限り、ユーザーはアクセスできません。パーミッションは Permission と Role ヘルパーで構築された文字列の配列です。
import 'package:appwrite/appwrite.dart';
// Permission と Role はメインパッケージのインポートに含まれています
パーミッション付きデータベース行
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]')), // 特定のユーザーが読取可能
Permission.update(Role.user('[USER_ID]')), // 特定のユーザーが更新可能
Permission.read(Role.team('[TEAM_ID]')), // すべてのチームメンバーが読取可能
Permission.read(Role.any()), // 誰でも (ゲストを含む) が読取可能
],
);
パーミッション付きファイルアップロード
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())を使用 — リソースを公開読取可能にします
ライセンス: BSD-3-Clause(寛容ライセンスのため全文を引用しています) · 原本リポジトリ
詳細情報
- 作者
- appwrite
- ライセンス
- BSD-3-Clause
- 最終更新
- 不明
Source: https://github.com/appwrite/agent-skills / ライセンス: BSD-3-Clause
関連スキル
doubt-driven-development
重要な判断はすべて、本番環境への展開前に新しい視点から対抗的レビューを実施します。速度より正確性が重要な場合、不慣れなコードを扱う場合、本番環境・セキュリティに関わるロジック・取り消し不可の操作など影響度が高い場合、または後でバグを修正するよりも今検証する方が効率的な場合に活用してください。
apprun-skills
TypeScriptを使用したAppRunアプリケーションのMVU設計に関する総合的なガイダンスが得られます。コンポーネントパターン、イベントハンドリング、状態管理(非同期ジェネレータを含む)、パラメータと保護機能を備えたルーティング・ナビゲーション、vistestを使用したテストに対応しています。AppRunコンポーネントの設計・レビュー、ルートの配線、状態フローの管理、AppRunテストの作成時に活用してください。
desloppify
コードベースのヘルスチェックと技術負債の追跡ツールです。コード品質、技術負債、デッドコード、大規模ファイル、ゴッドクラス、重複関数、コードスメル、命名規則の問題、インポートサイクル、結合度の問題についてユーザーが質問した場合に使用してください。また、ヘルススコアの確認、次の改善項目の提案、クリーンアップ計画の作成をリクエストされた際にも対応します。29言語に対応しています。
debugging-and-error-recovery
テストが失敗したり、ビルドが壊れたり、動作が期待と異なったり、予期しないエラーが発生したりした場合に、体系的な根本原因デバッグをガイドします。推測ではなく、根本原因を見つけて修正するための体系的なアプローチが必要な場合に使用してください。
test-driven-development
テスト駆動開発により実装を進めます。ロジックの実装、バグの修正、動作の変更など、あらゆる場面で活用できます。コードが正常に動作することを証明する必要がある場合、バグ報告を受けた場合、既存機能を修正する予定がある場合に使用してください。
incremental-implementation
変更を段階的に実施します。複数のファイルに影響する機能や変更を実装する場合に使用してください。大量のコードを一度に書こうとしている場合や、タスクが一度では完結できないほど大きい場合に活用します。