🚀 快速安装

复制以下命令并运行,立即安装此 Skill:

npx skills add https://skills.sh/appwrite/agent-skills/appwrite-typescript

💡 提示:需要 Node.js 和 NPM

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);

// 邮箱注册
await account.create({
    userId: ID.unique(),
    email: 'user@example.com',
    password: 'password123',
    name: 'User Name'
});

// 邮箱登录
const session = await account.createEmailPasswordSession({
    email: 'user@example.com',
    password: 'password123'
});

// OAuth 登录(Web)
account.createOAuth2Session({
    provider: OAuthProvider.Github,
    success: 'https://example.com/success',
    failure: 'https://example.com/fail',
    scopes: ['repo', 'user'] // 可选 —— 提供商特定的作用域
});

// 获取当前用户
const user = await account.get();

// 登出
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 scheme:

{
  "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) {
    // 创建在所有 Expo 环境中工作的深度链接
    const deepLink = new URL(makeRedirectUri({ preferLocalhost: true }));
    const scheme = `${deepLink.protocol}//`; // 例如 'exp://' 或 'appwrite-callback-[PROJECT_ID]://'

    // 获取 OAuth 登录 URL
    const loginUrl = await account.createOAuth2Token({
        provider,
        success: `${deepLink}`,
        failure: `${deepLink}`,
    });

    // 打开浏览器并监听 scheme 重定向
    const result = await WebBrowser.openAuthSessionAsync(`${loginUrl}`, scheme);

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

    // 从重定向 URL 中提取凭证
    const url = new URL(result.url);
    const secret = url.searchParams.get('secret');
    const userId = url.searchParams.get('userId');

    // 使用 OAuth 凭证创建会话
    await account.createSession({ userId, secret });
}

// 使用示例
await oauthLogin(OAuthProvider.Github);
await oauthLogin(OAuthProvider.Google);

用户管理(服务端)

const users = new Users(client);

// 创建用户
const user = await users.create({
    userId: ID.unique(),
    email: 'user@example.com',
    password: 'password123',
    name: 'User Name'
});

// 列出用户
const list = await users.list({ queries: [Query.limit(25)] });

// 获取用户
const fetched = await users.get({ userId: '[USER_ID]' });

// 删除用户
await users.delete({ userId: '[USER_ID]' });

数据库操作

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

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

const tablesDB = new TablesDB(client);

// 创建数据库(仅服务端)
const db = await tablesDB.create({ databaseId: ID.unique(), name: '我的数据库' });

// 创建表(仅服务端)
const col = await tablesDB.createTable({
    databaseId: '[DATABASE_ID]',
    tableId: ID.unique(),
    name: '我的表'
});

// 创建行
const doc = await tablesDB.createRow({
    databaseId: '[DATABASE_ID]',
    tableId: '[TABLE_ID]',
    rowId: ID.unique(),
    data: { title: 'Hello World', content: '示例内容' }
});

// 带查询条件列出行
const results = await tablesDB.listRows({
    databaseId: '[DATABASE_ID]',
    tableId: '[TABLE_ID]',
    queries: [Query.equal('status', 'active'), Query.limit(10)]
});

// 获取行
const 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: { title: '更新后的标题' }
});

// 删除行
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 的行大小限制。适用于短字段、需要索引的字段,如名称、别名或标识符。
  • textmediumtextlongtext 存储在页外(行中只保留一个 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 },  // 最多约 4 M 字符
        { key: 'raw_data', type: 'longtext',              required: false },  // 最多约 1 B 字符
    ]
});

TypeScript 泛型

import { Models } from 'appwrite';
// 服务端:从 'node-appwrite' 导入

// 为你的行数据定义类型接口
interface Todo {
    title: string;
    done: boolean;
    priority: number;
}

// listRows 默认返回 Models.DocumentList<Models.Document>
// 使用类型转换或泛型来获取类型化的结果
const results = await tablesDB.listRows({
    databaseId: '[DATABASE_ID]',
    tableId: '[TABLE_ID]',
    queries: [Query.equal('done', false)]
});

// 每个文档包含内置字段以及你的数据
const doc = results.documents[0];
doc.$id;            // string — 唯一行 ID
doc.$createdAt;     // string — ISO 8601 创建时间戳
doc.$updatedAt;     // string — ISO 8601 更新时间戳
doc.$permissions;   // string[] — 权限字符串
doc.$databaseId;    // string
doc.$collectionId;  // string

// 常见模型类型
// Models.User<Preferences>  — 用户账户
// Models.Session             — 认证会话
// Models.File                — 存储文件元数据
// Models.Team                — 团队对象
// Models.Execution           — 函数执行结果
// Models.DocumentList<T>     — 带总数计数的分页列表

查询方法

// 过滤
Query.equal('field', 'value')           // field == value(或传递数组用于 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 为 null
Query.isNotNull('field')                // field 不为 null
Query.startsWith('field', 'prefix')     // 字符串以 prefix 开头
Query.endsWith('field', 'suffix')       // 字符串以 suffix 结尾
Query.contains('field', 'substring')    // 字符串/数组包含值
Query.search('field', 'keywords')       // 全文搜索(需要全文索引)

// 排序
Query.orderAsc('field')                 // 升序排序
Query.orderDesc('field')                // 降序排序

// 分页
Query.limit(25)                         // 最大返回行数(默认 25,最大 100)
Query.offset(0)                         // 跳过 N 行
Query.cursorAfter('[ROW_ID]')           // 在此行 ID 之后分页(大数据集推荐)
Query.cursorBefore('[ROW_ID]')          // 在此行 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(查询默认使用 AND)

文件存储

const storage = new Storage(client);

// 上传文件(客户端 — 从文件输入)
const file = await storage.createFile({
    bucketId: '[BUCKET_ID]',
    fileId: ID.unique(),
    file: document.getElementById('file-input').files[0]
});

// 上传文件(服务端 — 从路径)
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')
});

// 列出文件
const files = await storage.listFiles({ bucketId: '[BUCKET_ID]' });

// 获取文件预览(图片)
const preview = storage.getFilePreview({
    bucketId: '[BUCKET_ID]',
    fileId: '[FILE_ID]',
    width: 300,
    height: 300
});

// 下载文件
const download = await storage.getFileDownload({
    bucketId: '[BUCKET_ID]',
    fileId: '[FILE_ID]'
});

// 删除文件
await storage.deleteFile({ bucketId: '[BUCKET_ID]', fileId: '[FILE_ID]' });

InputFile 工厂方法(服务端)

import { InputFile } from 'node-appwrite/file';

InputFile.fromPath('/path/to/file.png', 'file.png')          // 从文件系统路径
InputFile.fromBuffer(buffer, 'file.png')                       // 从 Buffer
InputFile.fromStream(readableStream, 'file.png', size)         // 从 ReadableStream(需要字节大小)
InputFile.fromPlainText('Hello world', 'hello.txt')            // 从字符串内容

团队管理

const teams = new Teams(client);

// 创建团队
const team = await teams.create({ teamId: ID.unique(), name: '工程团队' });

// 列出团队
const list = await teams.list();

// 创建成员资格(通过电子邮件邀请用户)
const membership = await teams.createMembership({
    teamId: '[TEAM_ID]',
    roles: ['editor'],
    email: 'user@example.com',
});

// 列出成员资格
const 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') 表示特定团队角色。

实时订阅(客户端)

import { Realtime, Channel } from 'appwrite';

const realtime = new Realtime(client);

// 订阅表行更改
const subscription = await realtime.subscribe(
    Channel.tablesdb('[DATABASE_ID]').table('[TABLE_ID]').row(),
    (response) => {
        console.log(response.events);   // 例如 ['tablesdb.*.tables.*.rows.*.create']
        console.log(response.payload);  // 受影响资源
    }
);

// 订阅特定行
await realtime.subscribe(
    Channel.tablesdb('[DATABASE_ID]').table('[TABLE_ID]').row('[ROW_ID]'),
    (response) => { /* ... */ }
);

// 订阅多个频道
await realtime.subscribe([
    Channel.tablesdb('[DATABASE_ID]').table('[TABLE_ID]').row(),
    Channel.bucket('[BUCKET_ID]').file(),
], (response) => { /* ... */ });

// 取消订阅
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);

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

// 列出执行记录
const executions = await functions.listExecutions({ functionId: '[FUNCTION_ID]' });

编写函数处理器(Node.js 运行时)

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

// src/main.js(或 src/main.ts)
export default async ({ req, res, log, error }) => {
    // 请求属性
    // req.body        — 原始请求体(字符串)
    // req.bodyJson    — 解析后的 JSON 体(对象,如果不是 JSON 则为 undefined)
    // req.headers     — 请求头(对象)
    // req.method      — HTTP 方法(GET、POST、PUT、DELETE、PATCH)
    // req.path        — URL 路径(例如 '/hello')
    // req.query       — 解析后的查询参数(对象)
    // req.queryString — 原始查询字符串

    log('处理请求:' + req.method + ' ' + req.path);

    if (req.method === 'GET') {
        return res.json({ message: '来自 Appwrite 函数的问候!' });
    }

    const data = req.bodyJson;
    if (!data?.name) {
        error('缺少 name 字段');
        return res.json({ error: 'name 字段为必填项' }, 400);
    }

    // 响应方法
    return res.json({ success: true });                    // JSON(自动设置 Content-Type)
    // return res.text('你好');                           // 纯文本
    // return res.empty();                                 // 204 No Content
    // return res.redirect('https://example.com');         // 302 重定向
    // return res.send('数据', 200, { 'X-Custom': '1' }); // 自定义正文、状态码、标头
};

服务端渲染(SSR)身份验证

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

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

// 管理员客户端(可重用)
const adminClient = new Client()
    .setEndpoint('https://<REGION>.cloud.appwrite.io/v1')
    .setProject('[PROJECT_ID]')
    .setKey(process.env.APPWRITE_API_KEY);

// 会话客户端(每个请求创建)
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 名称必须为 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: '未授权' });

    // 每个请求创建一个新的会话客户端
    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 流程

// 步骤 1:重定向到 OAuth 提供商
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);
});

// 步骤 2:处理回调 — 用令牌换取会话
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 安全: 始终使用 httpOnlysecuresameSite: 'strict' 以防止 XSS。Cookie 名称必须是 a_session_<PROJECT_ID>

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

错误处理

import { AppwriteException } from 'appwrite';
// 服务端:从 '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);   // 人类可读的错误消息
        console.log(err.code);      // HTTP 状态码(数字)
        console.log(err.type);      // Appwrite 错误类型字符串(例如 'document_not_found')
        console.log(err.response);  // 完整响应体(对象)
    }
}

常见错误码:

代码 含义
401 未授权 — 缺少或无效的会话/API 密钥
403 禁止访问 — 对此操作的权限不足
404 未找到 — 资源不存在
409 冲突 — 重复的 ID 或违反唯一约束
429 速率限制 — 请求过多,稍后重试

权限与角色(关键)

Appwrite 使用权限字符串来控制对资源的访问。每个权限将操作(readupdatedeletecreatewrite,后者授予创建 + 更新 + 删除)与角色目标配对。默认情况下,除非在文档/文件级别或从集合/存储桶设置继承权限,否则没有用户有权访问。权限是使用 PermissionRole 辅助函数构建的字符串数组。

import { Permission, Role } from 'appwrite';
// 服务端:从 '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]')),     // 特定用户可以读取
        Permission.update(Role.user('[USER_ID]')),   // 特定用户可以更新
        Permission.read(Role.team('[TEAM_ID]')),     // 所有团队成员可以读取
        Permission.read(Role.any()),                 // 任何人(包括访客)可以读取
    ]
});

带权限的文件上传

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()) — 使资源可公开读取

📄 原始文档

完整文档(英文):

https://skills.sh/appwrite/agent-skills/appwrite-typescript

💡 提示:点击上方链接查看 skills.sh 原始英文文档,方便对照翻译。

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。