🚀 快速安装
复制以下命令并运行,立即安装此 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 的行大小限制。适用于短字段、需要索引的字段,如名称、别名或标识符。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 }, // 最多约 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)使用服务端 SDK(node-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 安全: 始终使用
httpOnly、secure和sameSite: '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 使用权限字符串来控制对资源的访问。每个权限将操作(read、update、delete、create 或 write,后者授予创建 + 更新 + 删除)与角色目标配对。默认情况下,除非在文档/文件级别或从集合/存储桶设置继承权限,否则没有用户有权访问。权限是使用 Permission 和 Role 辅助函数构建的字符串数组。
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 原始英文文档,方便对照翻译。

评论(0)