关于 postgresql-development-cloudbase
postgresql-development-cloudbase 是指导 CloudBase PostgreSQL 应用开发的技能,面向在 CloudBase PG 环境中构建 Web 应用或 CMS 的开发者。它强调先确认环境后端,再按 PG 专用 API、迁移、授权与 RLS 流程实现数据访问。
主要功能
- 01确认环境是否启用 PostgreSQL
- 02区分 PG 与 NoSQL API
- 03通过迁移工作流管理数据库结构
- 04配置授权与行级安全策略
- 05指导前端 CRUD 与存储实现
适用场景
- 01为 Web 应用配置 CloudBase PG
- 02通过迁移创建或变更表结构
- 03设置 authenticated 用户的数据访问
- 04排查 PG 环境与存储配置问题
MCP 相关
该技能指导通过 MCP 的 queryEnv 检查环境,并使用 queryPgDatabase 检查数据、managePgDatabase 管理数据库。它要求通过迁移工作流应用结构变更。
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ tencentcloudbase/ skills --skill postgresql-development-cloudbase安装后在 Claude Code 中输入/postgresql-development-cloudbase确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
LICENSE.mdreferences/app-workflow.mdauth-and-rls.mdhttp-api.mdindex.mdpg-mode-overview.mdrealtime.mdrls-patterns.mdstorage-pg.mdtroubleshooting.md详细介绍
同级技能(仅本地)
同级 CloudBase 技能与此技能一起提供。使用 ../auth-tool-cloudbase/SKILL.md 这类本地相对路径。
如果此环境中缺少所引用的同级技能文件,请让用户安装完整的 CloudBase 插件(或缺少的技能)。不要通过 HTTP 将远程技能或协议 Markdown 拉取到 Agent 上下文中。
CloudBase PostgreSQL 开发
激活约定
以下情况请先使用本技能
- 任务提到 CloudBase PG、PostgreSQL、Postgres、PG 模式、RLS、JS SDK v3 PostgreSQL、
app.rdb()、queryPgDatabase或managePgDatabase。 - Web 应用或 CMS 必须将业务数据持久化到 CloudBase PostgreSQL,而不是 NoSQL 或 MySQL。
接着还要阅读
- 访问模式设计或慢查询工作 ->
../postgresql-best-practices-cloudbase/SKILL.md - Web 身份验证提供方准备情况 ->
../auth-tool-cloudbase/SKILL.md - Web 登录实现 ->
../auth-web-cloudbase/SKILL.md - 通用 Web 实现与验证 ->
../web-development/SKILL.md - 浏览器存储上传 ->
../cloud-storage-web/SKILL.md - 仅当 SDK 覆盖受阻时,才查阅原始 HTTP API 详情 ->
../http-api-cloudbase/SKILL.md - PG 参考索引 ->
references/index.md - PG 模式概览 ->
references/pg-mode-overview.md - 身份验证 / GRANT / RLS 详情 ->
references/auth-and-rls.md - PG 应用端到端闭环 ->
references/app-workflow.md - PG 存储详情 — 编写任何存储桶 / 上传 / URL 代码前必须阅读 ->
references/storage-pg.md - HTTP API 备用方案 ->
references/http-api.md - 故障排除 ->
references/troubleshooting.md
不要首先使用
relational-database-mcp-cloudbase/queryMysqlDatabase/manageMysqlDatabase:这些面向 MySQL。cloudbase-document-database-web-sdk/ 集合 API:不要用于必须存放在 CloudBase PG 中的业务数据。
必需流程
🚨 关键:PG 模式 API 与 NoSQL API 不同
CloudBase PG(app.rdb()、app.storage.from('bucket'))使用的 API 方法名称与 CloudBase NoSQL(app.database()、app.uploadFile())不同。能力较弱的模型经常会从训练数据中套用旧版 NoSQL / 身份验证代码片段;请立即拒绝这种做法。如果任务由 PG 提供支持,不要编写 app.database()、db.collection(...)、app.uploadFile()、getLoginState() 或基于 auth.getUser() 的路由守卫。请改用 app.rdb()、PG storage v3 和 auth.getSession()。如果你习惯使用其他 ORM 或 NoSQL 中的 .where()、.orderBy()、.count(),请停下来阅读下表。
| ❌ 不要使用这些(NoSQL / ORM 习惯用法) | ✅ PG 模式下请使用这些 |
|---|---|
.where({ field: value }) | .match({ field: value }) 或 .eq("field", value) |
.where("field", "ilike", "%v%") | .ilike("field", "%v%") |
.orderBy("field", { ascending: false }) | .order("field", { ascending: false }) |
.count() | .select("*", { count: "exact" }) — 计数包含在响应中 |
.offset(n) | .range(from, to) |
app.uploadFile()(旧版 NoSQL 上传) | app.storage.from('bucket').upload(key, file) |
app.getTempFileURL()(旧版 NoSQL URL) | app.storage.from('bucket').createSignedUrl(key, expiresIn) |
app.storage.from()(未指定存储桶名称) | app.storage.from('bucket') — 必须传入存储桶名称 |
如果你发现自己正在输入 .where()、.orderBy() 或 .count(),请停下来,使用右栏中的正确方法。
0. 首先,确认当前环境确实已配置 PostgreSQL。 调用 queryEnv(action="info", envId=...),并读取派生出的 EnvInfo.RuntimeBackends 块({ postgresql, nosql, mysql })以及 EnvInfo.RuntimeMode。仅当 RuntimeBackends.postgresql === true(等价于 EnvInfo.PostgreSQL 非空,和/或 EnvInfo.Meta 包含 postgresql=enable)时,才能安全地应用本技能中针对 PG 的指导。
- PG 模式是创建带 PostgreSQL 的 CloudBase 环境时选择的新环境模式。不要尝试原地将旧环境“升级”;请改为创建或选择 PG 模式环境。
- 如果
RuntimeBackends.postgresql === false,请停止——这是仅支持 NoSQL 的旧环境:浏览器端数据请改用cloudbase-document-database-web-sdk,上传请改用cloud-storage-web(配合app.uploadFile())。不要编写app.rdb()代码、启用 RLS,也不要在此处创建 pgstore 存储桶。 - 如果
postgresql和nosql均为true(PG 环境中的常见情况),两者会共存。对于任务要求存入 PG 的新业务数据(例如明确要求使用 PG 的文章/角色表),请应用本技能。现有 NoSQL 集合、EnvInfo.Storages[]中报告的存储桶,以及任何managePermissions(resourceType="noSqlDatabase")规则,仍然管理着旧版 NoSQL 数据——除非任务明确要求,否则不要迁移或重写这些内容。 RuntimeBackends.mysql === false是唯一明确表示“禁止使用”的信号:如果没有 MySQL,请勿使用manageMysqlDatabase/queryMysqlDatabase,也不要参考relational-database-mcp-cloudbase技能;这些内容仅适用于 MySQL,与 CloudBase PG 无关。- 注意:在 PG 环境中,
EnvInfo.Storages[]是旧版 NoSQL 存储桶。它仍可用于旧版app.uploadFile()流程,但不能作为可用的 pgstore 存储桶——绝不要在app.storage.from('<bucket>').upload('<key>', file)中复用它作为<bucket>部分。
创建 PG 模式环境
如果步骤 0 显示
RuntimeBackends.postgresql === false,但你需要 PostgreSQL,请创建一个启用了 PG 的新环境:
- 通过 MCP:
manageEnv(action="create", alias="my-env", packageId="baas_personal", resources=["storage","function","postgresql"], confirm="yes")— 可选的region(例如region="ap-shanghai")用于选择环境的创建位置;它会作为X-TC-Region请求上下文,因此不要在请求正文中填写Region。省略此参数则使用当前会话区域(站点默认值:国内为ap-shanghai,国际为ap-singapore);如果传入此参数,请在确认调用时再次传入。- 通过 CLI:
tcb env create --alias my-env --package baas_personal --postgresql --region ap-shanghai --yes- 通过控制台:创建环境
-
先检查现有的应用界面:
src/lib/backend.*、src/lib/auth.*、src/lib/*service.*、路由守卫,以及绑定到现有表单的处理程序。 -
通过 MCP 检查 PG 状态:使用
queryPgDatabase检查架构或读取数据,使用managePgDatabase执行 DDL/DML。不要切换到 MySQL 工具。要查看完整的路由映射,请阅读references/index.md。 -
编写代码前先了解 PG 角色: Publishable Key 对应
anon;已登录用户的访问令牌对应authenticated;API Key 对应service_role,并会绕过 RLS。切勿在前端代码中暴露 API Key /service_role凭据。请参阅references/auth-and-rls.md。 -
编写 CRUD 代码前,先使用架构管理工具(
managePgDatabase)。 架构 DDL(CREATE / ALTER / DROP / TRUNCATE)必须通过版本化迁移工作流执行——创建表时,绝不能默认使用execute。然后再应用 GRANT + RLS(通过execute或同一个迁移 SQL 包),之后才能通过浏览器访问。SQL 包至少应包含:CREATE TABLE、GRANT SELECT/INSERT/UPDATE/DELETE TO authenticated、使用serial/bigserial时的GRANT USAGE, SELECT ON SEQUENCE ... TO authenticated、ALTER TABLE ... ENABLE ROW LEVEL SECURITY,以及CREATE POLICY ... USING / WITH CHECK。完整模板请参阅references/auth-and-rls.md。默认架构变更工作流(先写本地文件,再写入远程历史):
- 将
migrationVersion设为 14 位 UTC 时间戳YYYYMMDDHHMMSS,将migrationName设为 snake_case(例如add_users)。 - 将 DDL 写入本地文件
cloudbase/migrations/<migrationVersion>_<migrationName>.sql(可选的回滚 SQL 可写在注释中或配套文件中)。此路径必须与 CloudBase CLIMIGRATIONS_DIR(tcb db pg migration *)一致。如果旧工作区仍使用根目录下的migrations/,请在混用 MCP 和 CLI 之前将其中的文件移至cloudbase/migrations/。 - 可选预览:
managePgDatabase(action=planMigration, migrationName=..., migrationVersion=..., sql=...)。 - 应用:
managePgDatabase(action=applyMigration, migrationName=..., migrationVersion=..., sql=..., confirm=true)— 使用与本地文件相同的版本号和名称。如果本地文件不存在,MCP 会自动写入cloudbase/migrations/<version>_<name>.sql;如果现有文件的内容与sql不一致,应用操作会安全失败(LOCAL_MIGRATION_FILE_MISMATCH),且不会 Push。默认情况下,MCP 会等待异步任务完成(最多 10 分钟,与 CLI 相同);如果主机工具调用超时较短,可通过taskPollTimeoutMs调整超时时间,或设置waitForTask=false。 - 验证:
managePgDatabase(action=listMigrations),确认远程历史记录了相同的migrationVersion。 - 然后再编写前端 CRUD / RLS 检查。
乱序 / 回填版本: 优先使用严格晚于
LatestVersion的migrationVersion。如果必须应用早于 Latest 的版本(分支合并 / cherry-pick),请在planMigration/applyMigration中传入includeAll=true— 与 CLItcb db pg migration up --include-all相同。日常工作中不要使用此选项。如果
applyMigration返回MIGRATION_TASK_TIMEOUT或MIGRATION_TASK_PENDING: 任务可能仍在运行(大型 DDL / 锁等待)。请先调用describeMigrationTask(taskId=...),查看 Status/Phase/Reason,然后调用listMigrations。不要再次 Push 相同的migrationVersion;在任务结束且listMigrations确认该版本从未应用之前,不要回退到execute。 其他迁移操作:managePgDatabase(action=migrationDetail, migrationVersion=...)— 检查单个迁移managePgDatabase(action=fetchMigration)— 将远程历史 SQL 拉取到cloudbase/migrations/(与 CLItcb db pg migration fetch保持一致)。可选指定migrationVersion以获取单个文件;省略则获取完整历史记录。除非设置force=true(覆盖文件/重新校准校验和),否则会跳过已有的本地文件。建议使用此操作,而不是手动从migrationDetail复制 SQL,以避免校验和不一致。managePgDatabase(action=repairMigration, migrationVersion=..., migrationName=..., repairStatus=..., repairReason=...)— 修复历史记录
execute用于 DML 和运维 SQL,默认不用于 DDL: 使用managePgDatabase(action=execute, confirm=true)执行INSERT/UPDATE/DELETE,以及不属于迁移的GRANT/CREATE POLICY/存储 RLS 操作。如果通过execute尝试执行架构 DDL,工具会软性拦截并提示DDL_USE_APPLY_MIGRATION,除非你显式设置allowDdlViaExecute=true(仅作为逃生通道)。 🚨 严重:在执行 CREATE TABLE 前检查表是否存在以及列名。 如果表已存在,CREATE TABLE IF NOT EXISTS会静默跳过,即使列名有误也一样。务必先调用queryPgDatabase(action="sql", sql="SELECT column_name, data_type FROM information_schema.columns WHERE table_name='xxx'"),检查表是否存在以及实际使用的准确列名。如果表已存在,但列名不匹配(例如使用user_id而不是uid),则必须:- 通过
applyMigration并使用新的版本执行ALTER TABLE,添加/重命名/删除列;或 - 使用
DROP TABLE IF EXISTS ... CASCADE,然后通过applyMigration重新创建表(仅当数据丢失可以接受时,例如在一次性/评估环境中)。 - 不要依赖
CREATE TABLE IF NOT EXISTS的静默跳过行为——这会导致所有下游 CRUD 查询因字段名错误而失败。 - 执行 DDL 后,重新查询架构,并核对前端代码、插入/更新负载、筛选条件、排序和 RLS 策略中使用的每个列名。
- 将
-
编写登录代码前,先检查用户名-密码认证:
- 调用
queryAppAuth(action="getLoginConfig")。 - 如果
loginMethods.usernamePassword !== true,调用manageAppAuth(action="patchLoginStrategy", patch={ usernamePassword: true })。 - 在 Web 登录代码中,对
admin或editor这类普通用户名使用auth.signInWithPassword({ username, password })。 - 不要假设
auth.signUp({ username, password })可以直接创建用户名/密码用户。先确认queryAppAuth的sdkHints和已安装的@cloudbase/js-sdk的行为;如果不支持直接通过用户名注册,应通过后端/管理边界实现注册,而不是在浏览器中暴露密钥。
- 调用
-
在编写 CRUD 前,使用
auth.getSession()实现 Web 认证状态:- 路由守卫必须检查
data.session,不能使用auth.getUser()或已弃用的getLoginState()。 - 仅当
signInWithPassword(...)返回的结果没有error且包含data.session时,才将登录视为成功。 - 从
data.session.user.id获取author_id/角色行的 UID(仅在检查实际 session 对象后,才回退使用sub/uid)。 - 不要使用
auth.getUser()作为登录凭证;没有真实的用户名/密码 session 时,它可能返回非空包装对象或看似匿名的用户数据。
- 路由守卫必须检查
-
浏览器端业务数据应优先使用 CloudBase JS SDK v3 的 PostgreSQL API:
app.rdb().from(table)。如果缺少app.rdb(xxx.rdb is not a function意味着 SDK 版本过旧),请使用最新版@cloudbase/js-sdk。 -
不要在浏览器代码中手动获取 CloudBase Auth Bearer 令牌用于 PG CRUD。尤其不要调用
currentUser.getIdToken()等非标准辅助方法,除非已确认该方法在已安装的 SDK 中确实存在。优先使用app.rdb(),由 SDK 传递当前有效 session。 -
在策略中使用官方 CloudBase PG SQL 认证辅助函数:使用
auth.uid()获取 JWTsub,使用auth.role()获取anon/authenticated/service_role,使用auth.jwt()获取完整声明,并在需要电子邮件时使用auth.email()。仍须通过真实应用 session 验证策略,再确认其可用:- 通过真实应用路径登录。
- 使用
author_id = session.user.id插入一条测试行。 - 使用
queryPgDatabase读取该行。 - 如果 INSERT/SELECT 失败,检查具体的 RLS 错误,并修复策略或改用服务端/RPC 边界。不要让面向浏览器的表保留无法正常工作的 RLS。
- ⚠️
auth.uid()返回text,而不是uuid。 所有者列优先使用varchar(64)/text。如果要与uuid列比较,请使用auth.uid()::uuid(仅当 JWTsub是有效 UUID 时),否则会出现operator does not exist: uuid = text。这与 Supabase 不同。请参阅references/auth-and-rls.md。 - ⚠️ 不要在 RLS 策略中使用
current_user或current_setting(...)。 PostgreSQL 中的current_user返回数据库角色名(例如authenticated),而不是 CloudBase 认证用户 ID。检查用户身份时始终使用auth.uid()。如果不确定是否有认证辅助函数可用,请运行SELECT proname FROM pg_proc WHERE pronamespace = 'auth'::regnamespace,列出所有可用的auth.*函数。
-
只有在阅读 OpenAPI 文档并确认已安装 SDK 中的认证模型后,才将 PG HTTP API 用作回退方案。不要猜测
/api/v1/rdb/rest这类 URL;文档指定的基址是https://<envId>.api.tcloudbasegateway.com/v1/rdb/rest/<table>,认证方式为Authorization: Bearer <Publishable Key | access_token | API Key>。 -
封面图片存储在 CloudBase Storage 中。PG 中仅保存最终文件 URL 或文件元数据。
-
声称完成前,验证两个层面:项目构建/类型检查,以及登录/CRUD 的浏览器 E2E;然后使用
queryPgDatabase回读数据行。调试 RLS 时,如果工具支持角色模拟,请以authenticated/anon身份运行 SQL;管理员/默认执行可能会绕过面向用户时遇到的故障。
探索预算
- 优先优化可正常使用的用户流程,再进行广泛研究。
- 如果任务是使用 PG 支持 CRUD 的 Web 应用,请先阅读
references/app-workflow.md,并遵循其中的闭环流程,然后再查询可选的 HTTP API 详情。 - 针对同一个问题,查询同一文档系列的次数不得超过两次。如果第二次查询仍无法解决问题,请检查已安装的 SDK 接口,或查看具体的运行时错误。
- 一旦选择使用
app.rdb()实现浏览器 CRUD,除非app.rdb()不存在或经证明确实失败,否则停止研究原始 PG HTTP API。 - DDL 执行失败后,最多重试 SQL 两次。然后调用
queryPgDatabase(action="objects")查找带模式限定的表名,再调用queryPgDatabase(action="schema", objectName="public.your_table"),查看确切错误,并简化模式或权限方案。 - 针对性修复时,避免陷入冗长的任务管理循环。读取当前文件,执行最少的平台设置,编辑代码并进行验证。
- 文件读取预算:同一文件不得读取超过 2 次。读取 2 次后,如果还需要重新读取,请使用
Grep进行定向搜索,或使用带有明确offset/limit的Read指定行范围。转而编辑或验证,不要继续重读文件。
数据模型规则
-
使用 CloudBase Auth / CloudBase PG 内置的身份验证身份作为用户来源。除非应用需要,否则不要额外复制一张身份表。
-
如果应用需要管理员/编辑者行为,请将业务角色保存在 PG 中,例如使用包含
uid、username和role的user_roles。uid的值必须与 Web 会话用作session.user.id的值相同,并且必须与使用的任何数据库策略表达式相匹配。 -
将内容表保存在 PG 中,例如包含所有者 UID 列的
articles或posts。 -
PG 表的物理列名优先使用 snake_case(
author_id、author_name、cover_image、created_at、updated_at)。如果 UI 字段使用 camelCase,请在服务边界处显式映射。 -
将
queryPgDatabase(action="schema", objectName="public.your_table")返回的架构视为事实来源。objectName为必填项,且必须包含架构限定名;如果尚不知道它,请先调用queryPgDatabase(action="objects")。如果现有表包含authorid/updatedat,要么在代码中使用这些确切列名,要么先显式迁移/删除并重建该表,再编写依赖author_id/updated_at的代码。 -
CREATE TABLE IF NOT EXISTS不会更改已有的不兼容架构。在评估或可丢弃的环境中,如果需要确定的架构,优先有意执行DROP TABLE IF EXISTS ... CASCADE,然后执行CREATE TABLE ...。 -
执行 DDL 后,再次查询表架构,并逐一核对前端代码、插入/更新负载、筛选条件、排序和 RLS 策略中使用的每一列。
-
数据库或服务/RPC 层必须实现后端权限控制。仅在 UI 中隐藏按钮是不够的。
-
不要让面向浏览器的表启用 RLS 却没有任何策略。在这种状态下,PostgreSQL 默认会拒绝用户读取/写入,因此
app.rdb().from("articles").insert(...)可能失败,而 UI 只显示通用的保存失败提示。如果启用 RLS,请在测试应用之前创建并验证 SELECT/INSERT/UPDATE/DELETE 策略。 -
在策略中使用 CloudBase PG 官方 SQL 身份验证辅助函数:
auth.uid()(JWTsub,返回text而不是uuid)、auth.role()(anon/authenticated/service_role)、auth.jwt()(完整声明)以及适用时的auth.email()。优先使用owner_id varchar(64) DEFAULT auth.uid()这样的所有者列,由数据库而非浏览器分配所有权。如果所有者列已是uuid,请使用auth.uid()::uuid进行比较(仅当sub是有效 UUID 时)。 -
标准所有者表模板——将此结构用于任何用户所有的业务表:
CREATE TABLE articles ( id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, owner_id TEXT NOT NULL DEFAULT auth.uid(), title TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now() );owner_id是TEXT,不是uuid——auth.uid()返回文本(例如EchhGXFadSANiCSaVim2wQ);将其声明为uuid会在建表时因类型不匹配而失败。owner_id带有DEFAULT auth.uid()——所有权由服务端决定。应用代码不得传入该值;INSERT 策略会拒绝伪造的所有者值。
-
填充演示数据: RLS 会拒绝匿名浏览器写入。请通过管理面插入演示/种子行——使用
managePgDatabase(action="execute", confirm=true)并提供显式的owner_id值(例如'system-demo');种子行不要依赖DEFAULT auth.uid(),也绝不要在前端代码中加入种子 INSERT。 -
如果需要详细的 GRANT/RLS 规则,请在编写策略前阅读
references/rls-patterns.md。 -
对于管理员/编辑者工作流,请允许
admin操作所有行,并仅允许editor操作所有者 UID 与当前用户匹配的行。
JS SDK v3 PostgreSQL 模式
表名规则(重要):
- ✅
db.from("articles")— 推荐 - ✅
db.from("public.articles")— 也有效(单个 schema 前缀) - ❌
db.from("public.public.articles")— 错误,重复添加 schema 前缀,会因PGRST205失败 queryPgDatabase()中的objectName="public.articles"是 MCP 工具格式——不要将其复制到db.from()中。
使用静态导入和一个共享的 app.rdb() 客户端(SDK 初始化参考:webv3/initialization.md):
import cloudbase from "@cloudbase/js-sdk";
const app = cloudbase.init({
env: import.meta.env.VITE_CLOUDBASE_ENV_ID,
accessKey: import.meta.env.VITE_PUBLISHABLE_KEY, // publishable key, see auth-web-cloudbase prerequisites
auth: { detectSessionInUrl: true },
});
export const auth = app.auth;
export const db = app.rdb();
最简身份验证辅助方法——只使用 auth.getSession(),绝不使用 auth.getUser():
async function getActiveSession() {
const { data, error } = await auth.getSession();
if (error || !data?.session || data.session.user?.is_anonymous) return null;
return data.session;
}
标准 CRUD 形式(请照原样复制):
// READ
const { data, error } = await db.from("articles").select("*");
// CREATE — omit owner_id/author_id when the table defines DEFAULT auth.uid()
const { data, error } = await db.from("articles").insert({ title, status: "draft" });
// UPDATE
await db.from("articles").update({ status }).eq("id", id);
// DELETE
await db.from("articles").delete().eq("id", id);
// RPC
const { data } = await db.rpc("function_name", { id });
常用查询辅助方法:.eq()、.neq()、.gt()、.gte()、.lt()、.lte()、.like()、.ilike()、.in()、.is()、.contains()、.textSearch()、.or()、.not()、.match()、.order()、.limit()、.range()、.single()。
完整指南(官方 webv3 API——请直接复制,不要从 .d.ts 重新推导)。 来源:webv3/postgresql/fetch.md——fetch / insert / update / delete / upsert / filters / modifiers / rpc 共用相同的路径前缀,每种操作各有一个页面(在 URL 后加 .md 可获取原始 Markdown):
// COUNT only — no rows returned, count comes back on the result object
const { count, error } = await db.from("articles").select("*", { count: "exact", head: true });
// Pagination — .range(from, to) is INCLUSIVE on both ends; page 2 of 20 = .range(20, 39)
const { data, error } = await db.from("articles").select("*")
.order("created_at", { ascending: false }).range(0, 19);
// INSERT and return the inserted row — ⚠️ .select() only returns rows when the
// table has a single auto-increment primary key; otherwise data is empty/null
const { data, error } = await db.from("articles").insert({ title, status: "draft" }).select();
// INSERT many rows at once (array form)
await db.from("articles").insert([{ title: "a" }, { title: "b" }]);
// UPSERT — include the primary key in values; onConflict names the unique-index column(s)
await db.from("articles").upsert({ id: 1, title: "new" }, { onConflict: "id" });
// Join query — PostgREST embedded resources via FK relationship
const { data, error } = await db.from("articles").select(`
title,
categories ( name ),
created_by:users!articles_created_by_fkey ( name ) // multiple FKs to the same table need the constraint name
`);
// RPC — SETOF-returning functions chain .select()/.order()/.limit()/.single()/filters like a query
const { data, error } = await db.rpc("search_articles", { keyword })
.select("title, published_at").order("published_at", { ascending: false }).limit(5);
const { data: one } = await db.rpc("search_articles", { keyword }).limit(1).single();
const { count } = await db.rpc("search_articles", { keyword }, { count: "exact", head: true });
⚠️ 重要:PG API 与 CloudBase NoSQL 或其他 ORM 并不相同
CloudBase PG(app.rdb())使用 postgREST 风格的查询辅助方法,不使用 CloudBase NoSQL(app.database())API,也不遵循常见的 ORM 约定。请勿使用:
| ❌ 错误(NoSQL / ORM 习惯) | ✅ 正确(postgREST / PG) |
|---|---|
.where({ field: value }) | .match({ field: value }) 或 .eq("field", value) |
.where("field", "ilike", "%v%") | .ilike("field", "%v%") |
.orderBy("field", { ascending: false }) | .order("field", { ascending: false }) |
.count() | .select("*", { count: "exact" }) — 使用 select 的 count 参数获取总数,返回结果中会包含 count 字段 |
.offset(n) | .range(from, to) — 注意,range 分页包含两端 |
黄金法则:app.rdb() 的查询链只使用上方“常用查询辅助方法”中列出的方法。如果你习惯写 .where() / .orderBy() / .count(),请立即改用对应的 postgREST 方法。Supabase 的 @supabase/postgrest-js 同样不使用这些方法名。
Storage(v3):使用 app.storage.from('<bucket>').upload('<key>', file)——复制前请先检查已安装 SDK 提供的接口:
const { data } = await app.storage.from('covers').upload(`${file.name}`, file);
存储桶必须存在(与 Supabase 保持一致)
CloudBase PG 存储使用 pgstore 后端,并遵循与 Supabase Storage 相同的模式:每次上传都必须指定一个已存在的存储桶。浏览器 SDK 无法创建存储桶。在编写任何上传代码之前:
- 确认目标前缀(例如
covers)对应的pgstore存储桶可用。DescribeEnvs.Storages[]暴露的旧版 NoSQL 存储桶(例如6d63-…-1409864723)供旧版 NoSQL 后端使用,无法用于pgstore上传。 - 如果没有可用的存储桶,请通过 PG 存储管理界面(PG 存储 HTTP API/CLI/控制台/适用时使用
storage.buckets上的 SQL)创建。不要假设传统模式的存储工具,或将covers/添加为 JS 路径前缀,就能创建 PG 存储桶。 - 存储桶名称应写在
from('<bucket>')中;传给upload(key, file)的键位于该存储桶内,不得重复存储桶前缀。正确:app.storage.from('covers').upload('a.png', file)。错误:app.storage.from('covers').upload('covers/a.png', file)。 - 创建存储桶后,通过
managePgDatabase(action="execute", confirm=true)配置storage.objects上的 RLS。默认 RLS 会拒绝所有访问;如果没有宽松策略,浏览器会收到STORAGE_PERMISSION_DENIED。完整的存储桶和 RLS 模板(用户隔离和公开读取存储桶)请参阅references/storage-pg.md,确切的 SQL 策略请参阅cloud-storage-web/SKILL.md中的“存储桶创建后:存储 RLS”部分。
故障模式速查表(在 DevTools 网络标签页中查看失败的 POST .../v1/storages/get-objects-upload-info 请求):
/v1/storages/get-objects-upload-info 返回的 code | 含义 | 修复方法 |
|---|---|---|
STORAGE_BUCKET_NOT_FOUND | 此 PG 环境中不存在路径里的存储桶。 | 通过管理界面创建存储桶,然后重试。 |
STORAGE_PERMISSION_DENIED | 存储桶存在,但 storage.objects 上的 RLS 阻止了上传。 | 运行 managePgDatabase(action="execute", confirm=true) 配置存储 RLS。请参阅 cloud-storage-web/SKILL.md 中的“存储桶创建后:存储 RLS”。 |
存储桶/键对应的 INVALID_PARAM | SDK/API 收到的存储桶/键组合无效(例如 from() 未提供存储桶,或键为空)。 | 使用 app.storage.from('covers').upload('a.png', file);存储桶写在 from() 中,键写在 upload() 中。 |
STORAGE_CONTENT_LENGTH_REQUIRED | 代码移除或遗漏了签名请求头 Content-Length。 | 为 uploadFile 传入 headers: { 'Content-Length': String(file.size) },或使用 app.storage.from('<bucket>').upload('<key>', file) 并传入 Blob/File,让 SDK 自动填充。 |
如果在 DevTools 中看到 PUT https://undefined/ 和 net::ERR_NAME_NOT_RESOLVED,这就是上述三种情况之一的症状——上游元数据响应没有 uploadUrl 字段,SDK 因此将 undefined 传给后续的 PUT 请求。务必先检查上游 get-objects-upload-info 响应;不要追查 https://undefined/ 这个 URL 本身。
硬性规则:绝不能让上传错误被静默吞掉。如果 uploadCoverImage() 被拒绝,外层的 createArticle() 流程也必须被拒绝——不要使用虚构的封面 URL 向 PG 插入数据,不要显示成功提示,也不要使用猜测的存储桶名称重试。
ExecutePGSql / DDL 故障排查
ExecutePGSql/managePgDatabase(action="execute")是管理/控制平面路径。不要在前端代码中暴露腾讯云 SecretKey 或 CloudBase API Key。- 每次调用执行一条 SQL 语句。请显式拆分批次,而不是发送以分号连接的多条语句。
- 部分 DDL(
CREATE/ALTER/DROP/GRANT/REVOKE/TRUNCATE/COMMENT)可能直接因瞬时InternalError而失败。发生这种情况时,请重试一次:将 DDL 包装在DO $$ BEGIN EXECUTE '...'; END $$中,并转义字符串中的单引号。 - 验证权限时,如果工具/API 支持角色参数,请使用面向用户的角色(
anon或authenticated)。默认的管理员执行可能会掩盖缺失的 GRANT/RLS 策略。
HTTP API 回退方案
- PG HTTP API 属于 CloudBase 关系型数据库 HTTP API 系列,与 MySQL 同属一类。在 MCP 文档/搜索中,它归在
mysqldb下。 - 编写原始
fetch()代码之前,请先查询 OpenAPI 文档:searchKnowledgeBase(mode="openapi", apiName="mysqldb", query="PostgreSQL fetch insert update rpc")。 - 不要凭记忆构造 REST 路径。猜测的路径返回 404 即构成阻断;请查阅表端点(
fetch/insert/update/delete)的 OpenAPI 契约。 - 数据库函数(RPC) 可用于执行原子化的服务端操作,例如带保护条件的计数器、条件更新或多步写入;普通 REST 更新无法安全实现这些操作。端点为与表端点使用同一网关基址的
POST /v1/rdb/rest/rpc/<function>;它不属于mysqldbOpenAPI 文档,因此在写入代码前,请先通过一次实际调用确认。 - 如果环境变量中提供了
TCB_HTTP_API_BASE_URL/VITE_TCB_HTTP_API_BASE_URL,请仅将其视为基址。路径、方法、请求头和认证模型仍须来自 OpenAPI 文档或现有的可用辅助函数。
前端防护措施
避免动态辅助函数陷阱:
- 不要编写
function getAuth() { return (await import("./backend")).auth; };请使用顶层静态导入,或将函数声明为async。 - 在 Vite 中不要编写
typeof import !== "undefined";请直接使用import.meta.env。 - Vite 报告转换错误后,不要继续编辑。先修复语法,重新运行构建,然后测试浏览器流程。
- 如果存在文档齐全的 v3 API,就不要花时间逆向分析无关的 SDK 内部实现。请先使用文档说明的
app.rdb()/app.storage.from()API。
快速检查
- PG 架构已创建,且与服务代码一致。
- 已启用用户名登录,代码使用用户名 API,而不是电子邮件 API。
- 数据写入通过 JS SDK v3
app.rdb()或有文档说明的 HTTP API 路径到达 CloudBase PG,而不是依赖本地状态、模拟数组或猜测后返回 404 的端点。 - 浏览器中的 PG 代码不得依赖
user.getIdToken()或虚构的令牌辅助函数。如果无法避免原始 HTTP,请先检查已安装的 CloudBase Web SDK/认证 API,并验证请求能通过当前用户会话成功执行。 - 编辑权限在 UI 之外强制执行。
- 与上传路径(例如
covers)匹配的 pgstore 存储桶必须在任何浏览器上传开始前创建。如果尚未创建,请通过管理界面创建;v3 SDK 不会替你创建。 - 存储上传会返回可用 URL,且该 URL 会与文章一同持久化。上传错误必须向上传播;不要使用占位封面 URL 插入文章行。
参考索引
所有打包的参考文件(技能 lint 可达性所必需):