Data API
Data API 是一个 Firebase 风格的文档数据库服务,允许第三方应用通过 HTTP 请求读写结构化 JSON 数据。每个应用可以创建多个数据集合(Collection),每个集合包含若干文档(Document)。
概览
| 项目 | 说明 |
|---|---|
| 基础路径 | /data/api/ |
| 认证方式 | Basic(应用模式)或 Bearer(用户模式) |
| 数据格式 | JSON |
| 文档大小限制 | 512 KB |
| 分页上限 | 每页最多 100 条 |
认证
Data API 支持两种认证模式,通过 Authorization 请求头区分:
应用模式(Basic)
使用应用的 client_id 和 client_secret 进行认证。应用模式拥有集合内所有文档的完整读写权限,不受权限配置限制。
Authorization: Basic base64(client_id:client_secret)⚠️ 安全提示:
client_secret拥有应用的完整管理权限。绝对不要将其写入分发给用户的客户端程序(游戏包体、桌面应用等),否则密钥可被逆向提取。应用模式仅应在受信任的服务器环境或内部开发工具中使用。**推荐架构:**面向用户的客户端应使用 Bearer 模式(用户 OIDC 登录),配合云函数和权限组实现业务逻辑。
用户模式(Bearer)
使用用户的 OAuth access_token 进行认证。用户模式的访问范围取决于集合的权限配置。
Authorization: Bearer <access_token>**🔒 访问说明:**数据集合默认为私有,用户无法通过 API 直接访问。如需客户端直接调用,请在开发者面板中将集合的
visibility设置为公开。推荐统一使用云函数处理数据操作。
重放防护
所有 API 请求必须携带以下请求头以防止重放攻击:
| 请求头 | 说明 |
|---|---|
X-Timestamp | 当前 Unix 时间戳(秒),与服务器时差不超过 ±5 分钟 |
X-Nonce | 随机唯一字符串,16~64 个字母数字字符,每次请求不同 |
可见性模型
每个集合通过 visibility(可见性位掩码)和 publicEditOp(公开编辑操作)控制用户模式的访问权限。
集合默认为私有——只有应用模式(Basic)和云函数可以访问。
visibility 位掩码
| 值 | 含义 |
|---|---|
0 | 私有(默认)——用户模式不可访问 |
1 | 公开只读——用户模式可读取文档,不可写入 |
3 | 公开读写——用户模式可读取,且可按 publicEditOp 规则写入 |
publicEditOp(编辑操作)
仅当 visibility = 3 时生效,控制用户模式下允许的写入类型:
| 值 | 含义 |
|---|---|
add | 仅允许新增/修改文档(PUT / PATCH) |
remove | 仅允许删除文档(DELETE) |
all | 允许新增、修改和删除(默认) |
应用模式(Basic 认证)不受可见性限制,始终可以读写所有文档。
集合管理
集合通过开发者管理面板创建和管理,不在 Data API 中提供公开接口。
每个合集可选填写 0~512 字符的合集描述,供开发者标记和注释用途。描述仅在管理接口、管理面板和开发工具中可见,不会通过公开 Data API 返回。
合集名称规则:
- 以字母或下划线开头
- 只允许字母、数字、下划线和连字符
- 长度 1~128 字符
- 正则:
/^[a-zA-Z_][a-zA-Z0-9_-]{0,127}$/
有序索引集合
普通集合保持旧行为:文档完整 JSON 存在 data_document.data,列表接口可按单个 JSON 字段排序。需要排行榜、竞速榜、价格榜等高频排序场景时,可以把集合的数据能力设为 indexed,并在 settings.indexes 中声明一个或多个 ordered 索引。
有序索引不会把整个榜单写进一个文档。仍然是“一个参赛实体 = 一个文档”,服务端会把 开发者指定的排序字段投影到内部 data_document_index 表中。这样单文档仍受 512 KB 限制, 但集合内文档数量可以持续增长,排行查询走索引投影。
示例:通用分区最佳分数榜,一个合集内可按 groupId + variant 拆分多个独立排行:
{
"indexes": [
{
"name": "best_score",
"kind": "ordered",
"partition": [
{ "path": "groupId", "type": "string" },
{ "path": "variant", "type": "string" }
],
"fields": [
{ "path": "score", "type": "int", "order": "desc" },
{ "path": "completedAt", "type": "int", "order": "asc" }
]
}
],
"writePolicy": {
"mode": "keepBest",
"index": "best_score"
}
}字段说明:
| 字段 | 说明 |
|---|---|
indexes[].name | 索引名,API 查询时使用 |
indexes[].kind | 当前支持 ordered |
indexes[].partition | 可选,分区字段。查询传入相同 partition 时,每个分区就是独立排行榜 |
fields[].path | 排序字段路径。默认指向 document.data,也支持 data.xxx、documentId、ownerUid、createdAt、updatedAt |
fields[].type | int、number、string 或 boolean |
fields[].order | asc 或 desc |
fields[].decimals | number 的小数缩放精度,0~6 |
fields[].required | 默认 true。设为 false 时缺失值按 missing 放到开头或末尾 |
writePolicy.mode | overwrite 或 keepBest |
writePolicy.index | keepBest 使用哪个 ordered index 判断新文档是否更靠前 |
keepBest 会在 PUT / PATCH / $data.set / $data.update / 管理面板运行脚本写入时生效: 如果新数据在指定索引下不比旧数据靠前,服务端保留旧文档并返回 kept: true。开发者管理 面板的单文档编辑用于维护数据,会直接写入并重建索引。
建议把参与排序的时间、分数、价格等关键值按业务需要标准化为整数,避免浮点排序误差。
管理面板运行指令
开发者管理面板提供“运行指令”工具,用于对应用数据库执行 SQL-like 命令或内嵌 JavaScript 脚本。该功能走管理端接口,需要登录用户拥有应用的 data 编辑权限;它不是 公开 Data API,不能用 Basic 应用密钥或用户 Bearer token 直接调用。
每次操作都可以选择:
| 模式 | 行为 |
|---|---|
preview | 只计算影响范围,返回 before / after 差异,不写入数据库 |
execute | 实际写入数据库,返回影响条数和本次变更差异 |
响应中的关键字段:
| 字段 | 说明 |
|---|---|
affected | 本次命中或修改的文档数量 |
result | 命令或脚本的返回值 |
changes | 写操作的差异列表,包含 operation、collection、documentId、before、after |
logs | 脚本中 console.log/info/warn/error 捕获的日志 |
maxRows 用于限制单次扫描/操作的文档数量,默认 100,最大 5000。
命令模式
命令模式不是完整 SQL,也不会执行原生 SQL;它只支持面向文档集合的安全子集:
SELECT * FROM players WHERE level >= 10 LIMIT 20;
UPDATE players SET reviewed = true, season = 'S1' WHERE score > 1000 LIMIT 100;
DELETE FROM players WHERE banned = true LIMIT 50;支持的条件操作符:
| 写法 | 含义 |
|---|---|
= | 等于 |
!= / <> | 不等于 |
> / >= | 大于 / 大于等于 |
< / <= | 小于 / 小于等于 |
字段名默认指向文档 data 中的顶层字段;也可以使用 documentId、ownerUid,或 data.profile.level 这样的嵌套路径。UPDATE SET 只能修改文档 data 字段,不能修改 documentId。
JS 脚本模式
脚本会在受控 VM 中执行,可使用内置 db 对象和 console。脚本必须通过 return 返回结果:
const docs = await db.select('players', {
where: { level: { $gte: 10 } },
limit: 100
})
for (const doc of docs) {
await db.update('players', {
where: { documentId: doc.documentId },
patch: { reviewed: true }
})
}
console.log('reviewed', docs.length)
return { reviewed: docs.length }可用方法:
| 方法 | 说明 |
|---|---|
db.select(collection, { where, limit, offset, sortBy, sortOrder }) | 查询文档,返回包含 documentId、ownerUid、data、时间戳的数组 |
db.update(collection, { where, patch, limit }) | 浅合并更新;patch 也支持 $set、$unset、$inc、$push、$pull |
db.delete(collection, { where, limit }) | 删除匹配文档 |
db.set(collection, documentId, data, { ownerUid }) | 创建或覆盖单个文档 |
db.indexTop(collection, indexName, { limit, offset, ownerUid, ownerUids, partition }) | 按有序索引读取前排或筛选后的排行 |
db.indexAround(collection, indexName, documentId, { before, after, partition }) | 读取指定文档所在的排名窗口 |
脚本 where 对象支持 { field: value } 和 { field: { $gt: 1 } } 形式,操作符包括 $eq、$ne、$gt、$gte、$lt、$lte、$in。字段路径规则与命令模式相同。
API 接口
列出文档
GET /data/api/{collectionName}查询参数:
| 参数 | 类型 | 说明 |
|---|---|---|
ownerUid | string | 可选,按所有者筛选 |
limit | number | 可选,每页条数,默认 50,最大 100 |
offset | number | 可选,偏移量,默认 0 |
sortBy | string | 可选,按 data 中的字段排序 |
sortOrder | string | 可选,排序方向:asc 或 desc |
where | string | 可选,JSON 过滤条件字符串(如 {"level":{"$gt":5}}) |
响应:
{
"documents": [
{
"documentId": "player_001",
"ownerUid": "R10001",
"createdAt": "1713168000",
"updatedAt": "1713168000"
}
],
"total": 42,
"limit": 50,
"offset": 0
}注意:
list不会返回data字段以节省带宽。data字段包含文档的全部 JSON 内容,可能很大。使用GET /data/api/{collectionName}/{documentId}获取完整文档。
按有序索引读取前 N 名
GET /data/api/{collectionName}/indexes/{indexName}/top查询参数:
| 参数 | 类型 | 说明 |
|---|---|---|
limit | number | 可选,默认 50,最大 100 |
offset | number | 可选,用于分页 |
ownerUid | string | 可选,只看单个 owner |
ownerUids | string | 可选,逗号分隔的 ownerUid 列表,适合好友榜 |
partition | JSON string | 可选,分区查询,例如 {"groupId":"item_001","variant":"hard"} |
响应会返回完整 data 和 rank:
{
"indexName": "best_score",
"documents": [
{
"rank": 1,
"documentId": "uid_001",
"ownerUid": "uid_001",
"data": {
"score": 1000000,
"label": "Entry 001",
"completedAt": 1713168000
},
"createdAt": "1713168000",
"updatedAt": "1713168000"
}
],
"total": 1,
"limit": 50,
"offset": 0
}获取自己或指定文档所在排行
GET /data/api/{collectionName}/indexes/{indexName}/me?before=5&after=5&partition={"groupId":"item_001","variant":"hard"}
GET /data/api/{collectionName}/indexes/{indexName}/around/{documentId}?before=5&after=5&partition={"groupId":"item_001","variant":"hard"}me 需要 Bearer 用户认证,并按当前用户 uid 查找该索引中最靠前的一条文档。 around/{documentId} 可用于应用模式或已知文档 ID 的场景。响应包含目标 rank、目标 document 以及前后窗口 documents。
获取文档
GET /data/api/{collectionName}/{documentId}响应:
{
"document": {
"documentId": "player_001",
"ownerUid": "R10001",
"data": { "score": 9800, "level": 15 },
"createdAt": "2026-03-10 12:00:00",
"updatedAt": "2026-03-15 08:30:00"
}
}
document.data才是你的业务 JSON 内容;外层document用于承载文档 ID、所有者和时间戳等元数据。
创建或覆盖文档
使用 PUT 语义——如果文档已存在则完全覆盖,不存在则创建。
PUT /data/api/{collectionName}/{documentId}
Content-Type: application/json请求体:
{
"data": { "score": 9800, "level": 15 },
"ownerUid": "R10001"
}| 字段 | 类型 | 说明 |
|---|---|---|
data | object | 必填,文档内容(任意 JSON 对象) |
ownerUid | string | 可选,仅应用模式下可指定文档所有者。用户模式下自动设置为当前用户 |
响应:
{
"status": "ok",
"documentId": "player_001",
"created": true
}文档 ID 规则:
- 只允许字母、数字、下划线、连字符、冒号和点
- 长度 1~128 字符
- 正则:
/^[a-zA-Z0-9_\-:.]{1,128}$/
**注意:**PUT 是整文档写入。文档已存在时,
data会被替换为请求体中的新对象; 没有出现在新data中的旧字段会被删除。PUT 不会因为缺少某个业务字段而报错。
部分更新文档
使用 PATCH 语义——对文档数据进行浅合并(Shallow Merge)。
PATCH /data/api/{collectionName}/{documentId}
Content-Type: application/json请求体:
{
"data": { "level": 16 }
}只更新指定的字段,其他字段保持不变。例如原文档的 score 不会被影响。
如果 PATCH 的 data 中包含文档原本没有的顶层字段,该字段会被自动新增;如果文档本身 不存在,则返回 404 not_found。PATCH 的浅合并只作用于顶层字段,不会自动把 "profile.name" 解析成嵌套路径。
响应:
{
"status": "ok",
"documentId": "player_001"
}删除文档
DELETE /data/api/{collectionName}/{documentId}响应:
{
"status": "ok"
}错误响应
所有错误以 JSON 格式返回,包含 error 和 error_description 字段:
{
"error": "not_found",
"error_description": "Document not found"
}常见错误码:
| HTTP 状态码 | error | 说明 |
|---|---|---|
| 400 | invalid_document_id | 文档 ID 格式不合法 |
| 400 | payload_too_large | 文档大小超过 512 KB 上限 |
| 401 | unauthorized | 缺少 Authorization 头 |
| 401 | invalid_client | Basic 认证失败(无效的 client_id 或 secret) |
| 401 | invalid_token | Bearer 令牌无效或已过期 |
| 403 | forbidden | 权限不足(例如用户尝试访问私有集合,或对只读集合执行写操作) |
| 403 | service_not_enabled | 应用未开通数据库服务 |
| 404 | not_found | 集合或文档不存在 |
示例
cURL — 应用模式写入
# 写入文档
curl -X PUT https://api.starecho.net/data/api/player_data/player_001 \
-H "Authorization: Basic $(echo -n 'your_client_id:your_client_secret' | base64)" \
-H "Content-Type: application/json" \
-d '{"data": {"score": 9800, "level": 15}, "ownerUid": "R10001"}'
# 读取文档
curl https://api.starecho.net/data/api/player_data/player_001 \
-H "Authorization: Basic $(echo -n 'your_client_id:your_client_secret' | base64)"cURL — 用户模式读写
# 用户写入自己的文档
curl -X PUT https://api.starecho.net/data/api/player_data/my_settings \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..." \
-H "Content-Type: application/json" \
-d '{"data": {"theme": "dark", "language": "zh-CN"}}'
# 用户读取自己的文档
curl https://api.starecho.net/data/api/player_data/my_settings \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..."JavaScript(Fetch API)
const CLIENT_ID = 'your_client_id'
const CLIENT_SECRET = 'your_client_secret'
const BASE_URL = 'https://api.starecho.net/data/api'
// 写入文档
await fetch(`${BASE_URL}/player_data/player_001`, {
method: 'PUT',
headers: {
'Authorization': `Basic ${btoa(`${CLIENT_ID}:${CLIENT_SECRET}`)}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
data: { score: 9800, level: 15 },
ownerUid: 'R10001',
}),
})
// 列出文档
const res = await fetch(`${BASE_URL}/player_data?limit=20&offset=0`, {
headers: {
'Authorization': `Basic ${btoa(`${CLIENT_ID}:${CLIENT_SECRET}`)}`,
},
})
const { documents, total } = await res.json()