Packages 包@downcity/city
Store 与 Table
City 的数据层、表能力和为什么 town、env、usage 最终都会落到 store 层。
Downcity 最常被看见的是 AI 调用,但它能长期稳定运行,靠的是一套明确的数据层。
这个概念在说什么
很多 City 能力都不是纯内存状态,它们最终都必须落到数据事实:
- town 是否 active
- 某个 token 属于谁
- 某个 env key 当前值是什么
- 某次 service 调用是否已记录 usage
- 某个用户是否已经拿到 entitlement
这就是为什么 @downcity/city 里不仅有 HTTP 路由,还有 store / table 这一层。
什么时候你需要直接关心这一层
- 你在排查 service 有没有真的写表
- 你要把 City 数据和自己的业务表联动
- 你想从代码里读写某些受控表
- 你在做运维排查,不想只看 HTTP 返回
base.table() 代表什么
base.table(name) 不是“随便暴露数据库”,而是 City 对底层数据库能力的受控暴露。
它给你的不是随意 SQL 字符串,而是围绕 City 生命周期统一管理的表接口。
最小示例:读写 City 表
const towns = await base.table("towns");
const env = await base.table("env");
const notes = await base.table("notes.notes");
await notes.insert({
id: "note_1",
title: "First note",
status: "draft",
});
const draftNotes = await notes.select({
status: "draft",
});这段代码说明三件事:
- City 内置表可以直接拿
- 你自己的 service 表也可以通过统一入口拿
- 数据操作最终受 runtime 和 City 生命周期治理
你通常会碰到哪些表
City 内置表
townsenv
它们支撑的是:
- 产品状态
- 运行时环境变量
- City 的管理基础设施
service 表
不同 service 会带自己的表,例如:
- accounts 相关用户 / session 表
- usage 事件表
- Stripe entitlement / webhook 事件表
你自己的业务表
如果你在 service 里定义了业务表,也会进入同一个统一 runtime 数据层。
常见场景
场景一:确认数据到底有没有落进去
const usageEvents = await base.table("service_usage_events");
const rows = await usageEvents.select();这类场景常见于:
- usage 为什么看起来没记上
- Stripe webhook 进来了没有
- 账号注册后表里有没有数据
场景二:程序里做受控更新
const envTable = await base.table("env");
await envTable.update({
where: { key: "OPENAI_API_KEY" },
values: { value: "new-secret" },
});适合:
- 可信环境里的受控维护
- 内部脚本
- 管理后台后端逻辑
场景三:删除或清理某类数据
const notes = await base.table("notes.notes");
await notes.delete({ id: "note_1" });适合:
- 业务清理
- 测试环境重置
- 服务联动逻辑
常用 API / 入口
这一层最重要的 API 很少:
await base.table(name)table.select(where?)table.insert(values)table.update({ where, values })table.delete(where)
如果你想看更底层方法签名,去读 Runtime API 和 City API。
常见误解
City 不只是 API 代理
它后面还有长期存在的数据层,很多管理能力都建立在这些表上。
table() 不是让你跳过 City 设计
更推荐先通过:
- service
- service
- Admin City
来表达业务入口;table() 更适合可信环境和受控排查。
不是所有表都应该直接给前端碰
前端产品应该继续走 User City / Admin City,而不是直接访问底层表。