mcpField
A
appwrite-typescript

appwrite-typescript

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

关于 appwrite-typescript

面向 TypeScript 开发者的 Appwrite 客户端与服务端 SDK 指南,涵盖身份验证、表格数据库、存储、实时订阅、函数和访问控制。

主要功能

  1. 01Web、React Native 与服务端 SDK 配置
  2. 02身份验证与 SSR 会话模式
  3. 03TablesDB 查询和类型化行数据
  4. 04文件存储与实时订阅
  5. 05函数、团队和权限管理

适用场景

  1. 01为应用接入 Appwrite 身份验证
  2. 02使用 TypeScript 构建类型化表格操作
  3. 03上传文件并订阅资源变更

安装

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

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

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

详细介绍

Appwrite TypeScript SDK

安装

# Web
npm install appwrite

# React Native
npm install react-native-appwrite

# Node.js / Deno
npm install node-appwrite

设置客户端

客户端(Web / React Native)
// Web
import { Client, Account, TablesDB, Storage, ID, Query } from 'appwrite';

// React Native
import { Client, Account, TablesDB, Storage, ID, Query } from 'react-native-appwrite';

const client = new Client()
    .setEndpoint('https://<REGION>.cloud.appwrite.io/v1')
    .setProject('[PROJECT_ID]');
服务端(Node.js / Deno)
import { Client, Users, TablesDB, Storage, Functions, ID, Query } from 'node-appwrite';

const client = new Client()
    .setEndpoint('https://<REGION>.cloud.appwrite.io/v1')
    .setProject(process.env.APPWRITE_PROJECT_ID)
    .setKey(process.env.APPWRITE_API_KEY);

代码示例

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

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

// Email login
const session = await account.createEmailPasswordSession({
    email: '[email protected]',
    password: 'password123'
});

// OAuth login (Web)
account.createOAuth2Session({
    provider: OAuthProvider.Github,
    success: 'https://example.com/success',
    failure: 'https://example.com/fail',
    scopes: ['repo', 'user'] // optional — provider-specific scopes
});

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

// Logout
await account.deleteSession({ sessionId: 'current' });
OAuth 2 登录(React Native)

重要: createOAuth2Session() 在 React Native 上无法使用。你必须改用 createOAuth2Token() 并配合深层链接。

设置

安装所需依赖:

npx expo install react-native-appwrite react-native-url-polyfill
npm install expo-auth-session expo-web-browser expo-linking

在 app.json 中设置 URL 方案:

{
  "expo": {
    "scheme": "appwrite-callback-[PROJECT_ID]"
  }
}
OAuth 流程
import { Client, Account, OAuthProvider } from 'react-native-appwrite';
import { makeRedirectUri } from 'expo-auth-session';
import * as WebBrowser from 'expo-web-browser';

const client = new Client()
    .setEndpoint('https://<REGION>.cloud.appwrite.io/v1')
    .setProject('[PROJECT_ID]');

const account = new Account(client);

async function oauthLogin(provider: OAuthProvider) {
    // Create deep link that works across Expo environments
    const deepLink = new URL(makeRedirectUri({ preferLocalhost: true }));
    const scheme = `${deepLink.protocol}//`; // e.g. 'exp://' or 'appwrite-callback-[PROJECT_ID]://'

    // Get the OAuth login URL
    const loginUrl = await account.createOAuth2Token({
        provider,
        success: `${deepLink}`,
        failure: `${deepLink}`,
    });

    // Open browser and listen for the scheme redirect
    const result = await WebBrowser.openAuthSessionAsync(`${loginUrl}`, scheme);

    if (result.type !== 'success') return;

    // Extract credentials from the redirect URL
    const url = new URL(result.url);
    const secret = url.searchParams.get('secret');
    const userId = url.searchParams.get('userId');

    // Create session with the OAuth credentials
    await account.createSession({ userId, secret });
}

// Usage
await oauthLogin(OAuthProvider.Github);
await oauthLogin(OAuthProvider.Google);
用户管理(服务端)
const users = new Users(client);

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

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

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

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

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

提示: 所有 SDK 方法调用都应优先采用对象参数调用方式(例如,{ databaseId: '...' })。只有在现有代码库已采用位置参数或用户明确要求时,才使用位置参数。

const tablesDB = new TablesDB(client);

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

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

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

// List rows with query
const results = await tablesDB.listRows({
    databaseId: '[DATABASE_ID]',
    tableId: '[TABLE_ID]',
    queries: [Query.equal('status', 'active'), Query.limit(10)]
});

// Get row
const 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: { title: 'Updated Title' }
});

// 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
    ]
});
TypeScript 泛型
import { Models } from 'appwrite';
// Server-side: import from 'node-appwrite'

// Define a typed interface for your row data
interface Todo {
    title: string;
    done: boolean;
    priority: number;
}

// listRows returns Models.DocumentList<Models.Document> by default
// Cast or use generics for typed results
const results = await tablesDB.listRows({
    databaseId: '[DATABASE_ID]',
    tableId: '[TABLE_ID]',
    queries: [Query.equal('done', false)]
});

// Each document includes built-in fields alongside your data
const doc = results.documents[0];
doc.$id;            // string — unique row ID
doc.$createdAt;     // string — ISO 8601 creation timestamp
doc.$updatedAt;     // string — ISO 8601 update timestamp
doc.$permissions;   // string[] — permission strings
doc.$databaseId;    // string
doc.$collectionId;  // string

// Common model types
// Models.User<Preferences>  — user account
// Models.Session             — auth session
// Models.File                — storage file metadata
// Models.Team                — team object
// Models.Execution           — function execution result
// Models.DocumentList<T>     — paginated list with total count
查询方法
// Filtering
Query.equal('field', 'value')           // field == value (or pass array for IN)
Query.notEqual('field', 'value')        // field != value
Query.lessThan('field', 100)            // field < value
Query.lessThanEqual('field', 100)       // field <= value
Query.greaterThan('field', 100)         // field > value
Query.greaterThanEqual('field', 100)    // field >= value
Query.between('field', 1, 100)          // 1 <= field <= 100
Query.isNull('field')                   // field is null
Query.isNotNull('field')                // field is not null
Query.startsWith('field', 'prefix')     // string starts with prefix
Query.endsWith('field', 'suffix')       // string ends with suffix
Query.contains('field', 'substring')    // string/array contains value
Query.search('field', 'keywords')       // full-text search (requires full-text index)

// Sorting
Query.orderAsc('field')                 // sort ascending
Query.orderDesc('field')                // sort descending

// Pagination
Query.limit(25)                         // max rows returned (default 25, max 100)
Query.offset(0)                         // skip N rows
Query.cursorAfter('[ROW_ID]')           // paginate after this row ID (preferred for large datasets)
Query.cursorBefore('[ROW_ID]')          // paginate before this row ID

// Selection
Query.select(['field1', 'field2'])      // return only specified fields

// Logical
Query.or([Query.equal('a', 1), Query.equal('b', 2)])   // OR condition
Query.and([Query.greaterThan('age', 18), Query.lessThan('age', 65)])  // explicit AND (queries are AND by 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', '$id', 'customerId')
Query.leftJoin('orders', '$id', 'customerId', '=', 'ord')
Query.leftJoin('orders', 'ord', [
    Query.on('$id', 'customerId'),
    Query.equal('ord.status', 'paid'),
])

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

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

// Upload file (client-side — from file input)
const file = await storage.createFile({
    bucketId: '[BUCKET_ID]',
    fileId: ID.unique(),
    file: document.getElementById('file-input').files[0]
});

// Upload file (server-side — from path)
import { InputFile } from 'node-appwrite/file';

const file2 = await storage.createFile({
    bucketId: '[BUCKET_ID]',
    fileId: ID.unique(),
    file: InputFile.fromPath('/path/to/file.png', 'file.png')
});

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

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

// Download file
const download = await storage.getFileDownload({
    bucketId: '[BUCKET_ID]',
    fileId: '[FILE_ID]'
});

// Delete file
await storage.deleteFile({ bucketId: '[BUCKET_ID]', fileId: '[FILE_ID]' });
InputFile 工厂方法(服务端)
import { InputFile } from 'node-appwrite/file';

InputFile.fromPath('/path/to/file.png', 'file.png')          // from filesystem path
InputFile.fromBuffer(buffer, 'file.png')                       // from Buffer
InputFile.fromStream(readableStream, 'file.png', size)         // from ReadableStream (size in bytes required)
InputFile.fromPlainText('Hello world', 'hello.txt')            // from string content
团队
const teams = new Teams(client);

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

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

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

// List memberships
const 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') 指定特定团队角色。

实时订阅(客户端)
import { Realtime, Channel } from 'appwrite';

const realtime = new Realtime(client);

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

// Subscribe to a specific row
await realtime.subscribe(
    Channel.tablesdb('[DATABASE_ID]').table('[TABLE_ID]').row('[ROW_ID]'),
    (response) => { /* ... */ }
);

// Subscribe to multiple channels
await realtime.subscribe([
    Channel.tablesdb('[DATABASE_ID]').table('[TABLE_ID]').row(),
    Channel.bucket('[BUCKET_ID]').file(),
], (response) => { /* ... */ });

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

response 对象包含:events(事件字符串数组)、payload(受影响的资源)、channels(匹配的频道)和 timestamp(ISO 8601)。

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

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

// List executions
const executions = await functions.listExecutions({ functionId: '[FUNCTION_ID]' });
编写函数处理程序(Node.js 运行时)

部署自己的 Appwrite Function 时,入口文件必须导出一个默认的异步函数:

// src/main.js (or src/main.ts)
export default async ({ req, res, log, error }) => {
    // Request properties
    // req.body        — raw request body (string)
    // req.bodyJson    — parsed JSON body (object, or undefined if not JSON)
    // req.headers     — request headers (object)
    // req.method      — HTTP method (GET, POST, PUT, DELETE, PATCH)
    // req.path        — URL path (e.g. '/hello')
    // req.query       — parsed query parameters (object)
    // req.queryString — raw query string

    log('Processing request: ' + req.method + ' ' + req.path);

    if (req.method === 'GET') {
        return res.json({ message: 'Hello from Appwrite Function!' });
    }

    const data = req.bodyJson;
    if (!data?.name) {
        error('Missing name field');
        return res.json({ error: 'Name is required' }, 400);
    }

    // Response methods
    return res.json({ success: true });                    // JSON (sets Content-Type automatically)
    // return res.text('Hello');                           // plain text
    // return res.empty();                                 // 204 No Content
    // return res.redirect('https://example.com');         // 302 Redirect
    // return res.send('data', 200, { 'X-Custom': '1' }); // custom body, status, headers
};
服务端渲染(SSR)身份验证

SSR 应用(Next.js、SvelteKit、Nuxt、Remix、Astro)使用服务端 SDK(node-appwrite)处理身份验证。你需要两个客户端:

  • 管理员客户端 — 使用 API 密钥、创建会话、绕过速率限制(可复用的单例)
  • 会话客户端 — 使用会话 Cookie,代表用户执行操作(每个请求创建一个,绝不共享)
import { Client, Account, OAuthProvider } from 'node-appwrite';

// Admin client (reusable)
const adminClient = new Client()
    .setEndpoint('https://<REGION>.cloud.appwrite.io/v1')
    .setProject('[PROJECT_ID]')
    .setKey(process.env.APPWRITE_API_KEY);

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

const session = req.cookies['a_session_[PROJECT_ID]'];
if (session) {
    sessionClient.setSession(session);
}
邮箱/密码登录
app.post('/login', async (req, res) => {
    const account = new Account(adminClient);
    const session = await account.createEmailPasswordSession({
        email: req.body.email,
        password: req.body.password,
    });

    // Cookie name must be a_session_<PROJECT_ID>
    res.cookie('a_session_[PROJECT_ID]', session.secret, {
        httpOnly: true,
        secure: true,
        sameSite: 'strict',
        expires: new Date(session.expire),
        path: '/',
    });

    res.json({ success: true });
});
已验证请求
app.get('/user', async (req, res) => {
    const session = req.cookies['a_session_[PROJECT_ID]'];
    if (!session) return res.status(401).json({ error: 'Unauthorized' });

    // Create a fresh session client per request
    const sessionClient = new Client()
        .setEndpoint('https://<REGION>.cloud.appwrite.io/v1')
        .setProject('[PROJECT_ID]')
        .setSession(session);

    const account = new Account(sessionClient);
    const user = await account.get();
    res.json(user);
});
OAuth2 SSR 流程
// Step 1: Redirect to OAuth provider
app.get('/oauth', async (req, res) => {
    const account = new Account(adminClient);
    const redirectUrl = await account.createOAuth2Token({
        provider: OAuthProvider.Github,
        success: 'https://example.com/oauth/success',
        failure: 'https://example.com/oauth/failure',
    });
    res.redirect(redirectUrl);
});

// Step 2: Handle callback — exchange token for session
app.get('/oauth/success', async (req, res) => {
    const account = new Account(adminClient);
    const session = await account.createSession({
        userId: req.query.userId,
        secret: req.query.secret,
    });

    res.cookie('a_session_[PROJECT_ID]', session.secret, {
        httpOnly: true, secure: true, sameSite: 'strict',
        expires: new Date(session.expire), path: '/',
    });
    res.json({ success: true });
});

**Cookie 安全性:**始终使用 httpOnly、secure 和 sameSite: 'strict',以防止 XSS。Cookie 名称必须为 a_session_<PROJECT_ID>。

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

错误处理

import { AppwriteException } from 'appwrite';
// Server-side: import from 'node-appwrite'

try {
    const doc = await tablesDB.getRow({
        databaseId: '[DATABASE_ID]',
        tableId: '[TABLE_ID]',
        rowId: '[ROW_ID]',
    });
} catch (err) {
    if (err instanceof AppwriteException) {
        console.log(err.message);   // human-readable error message
        console.log(err.code);      // HTTP status code (number)
        console.log(err.type);      // Appwrite error type string (e.g. 'document_not_found')
        console.log(err.response);  // full response body (object)
    }
}

常见错误代码:

代码含义
401未授权 — 缺少或无效的会话/API 密钥
403禁止访问 — 执行此操作的权限不足
404未找到 — 资源不存在
409冲突 — ID 重复或违反唯一性约束
429请求过于频繁 — 请求过多,请等待退避后重试

权限与角色(重要)

Appwrite 使用权限字符串控制对资源的访问。每项权限都将一个操作(read、update、delete、create,或授予创建、更新和删除权限的 write)与一个角色目标配对。默认情况下,除非在行/文件级别显式设置权限,或从表/存储桶设置继承权限,否则任何用户都无法访问。权限是由 Permission 和 Role 助手构建的字符串数组。

import { Permission, Role } from 'appwrite';
// Server-side: import from 'node-appwrite'
带权限的数据库行
const 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
    ]
});
带权限的文件上传
const file = await storage.createFile({
    bucketId: '[BUCKET_ID]',
    fileId: ID.unique(),
    file: document.getElementById('file-input').files[0],
    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