Context
ObjectStack 使用 project-per-database 隔离模型,每个 project 对应一个独立的 Turso SQLite 数据库。用户希望实现类似 Git branch 的功能:从一个 production project fork 出独立的子数据库(branch),用于开发环境、PR 预览、迁移测试等场景。
Turso 原生支持通过 seed 参数瞬间 fork 数据库,可直接利用此能力。
用户决策:
- Branch 创建:全量 Fork(含数据),使用 Turso seed 参数
- Branch 删除:软删除 + 延迟清理(先 archived,N 天后物理删除)
Critical Files
| 文件 |
角色 |
packages/spec/src/cloud/project.zod.ts |
Schema 定义(Zod 优先) |
packages/services/service-tenant/src/project-provisioning.ts |
Branch 核心逻辑 |
packages/services/service-tenant/src/turso-platform-client.ts |
Turso fork API 调用 |
packages/services/service-tenant/src/objects/sys-project.object.ts |
ObjectQL 字段定义 |
packages/rest/src/branch-routes.ts |
REST 路由(新文件) |
packages/rest/src/rest-api-plugin.ts |
注册 branch routes |
packages/cli/src/commands/projects/branch-create.ts |
CLI(新文件) |
packages/cli/src/commands/projects/branch-list.ts |
CLI(新文件) |
packages/cli/src/commands/projects/branch-delete.ts |
CLI(新文件) |
Implementation Steps
Step 1 — Spec Schema (packages/spec/src/cloud/project.zod.ts)
在 ProjectSchema 中添加 branch 可选字段:
parentProjectId: z.string().uuid().optional(),
branchName: z.string().min(1).max(100).optional(),
sourceDatabaseName: z.string().optional(),
branchStatus: z.enum(['active', 'merged', 'deleted', 'diverged']).optional(),
forkedAt: z.string().datetime().optional(),
scheduledDeleteAt: z.string().datetime().optional(), // 软删除用
新增独立 schema:
export const BranchProjectRequestSchema = z.object({
parentProjectId: z.string().uuid(),
branchName: z.string().min(1).max(100),
displayName: z.string().optional(),
createdBy: z.string(),
metadata: z.record(z.string(), z.unknown()).optional(),
});
export type BranchProjectRequest = z.infer<typeof BranchProjectRequestSchema>;
从 packages/spec/src/cloud/index.ts 导出新类型。
Step 2 — Turso Platform Client (packages/services/service-tenant/src/turso-platform-client.ts)
确认 createDatabase 已支持 seed 参数(代码中已有),若无则补充:
createDatabase(params: {
name: string;
group?: string;
seed?: { type: 'database'; name: string };
is_schema?: boolean;
}): Promise<{ ...}>
Step 3 — 数据库适配器扩展 (packages/services/service-tenant/src/project-provisioning.ts)
3a. 扩展 ProjectDatabaseAdapter 接口:
forkDatabase?(params: {
projectId: string;
sourceDatabaseName: string;
targetDatabaseName: string;
region: string;
storageLimitMb: number;
}): Promise<{ databaseUrl: string; plaintextSecret: string }>;
deleteDatabase(databaseName: string): Promise<void>;
3b. 在 TursoProjectDatabaseAdapter 中实现 forkDatabase:
调用 client.createDatabase({ name: target, seed: { type: 'database', name: source } }),再 createDatabaseToken,返回 url + token。
3c. 添加 branchProject 方法(原子性保证):
1. 查找父 project 行(获取 databaseUrl, driver)
2. 检查 adapter.forkDatabase 是否存在,否则 422
3. 派生命名:deriveBranchDatabaseName(parentDbName, branchName)
4. 调用 adapter.forkDatabase() —— 若失败直接抛出,不写任何 DB 行
5. fork 成功后:
a. 写 sys_project 行(含 branch 字段)
b. 写 sys_project_credential 行(加密 token)
c. 若 5a/5b 失败:catch 后调用 adapter.deleteDatabase() 补偿,再重新抛出
6. 返回 { project, credential, durationMs }
命名工具函数:
function deriveBranchDatabaseName(parentName: string, branchName: string): string {
const cleanBranch = branchName.toLowerCase().replace(/[^a-z0-9]/g, '-');
const suffix = `-br-${cleanBranch}`;
return `${parentName.slice(0, 26 - suffix.length)}${suffix}`.slice(0, 26);
}
function extractDatabaseName(databaseUrl: string): string {
// libsql://db-name.turso.io → db-name
return databaseUrl.replace(/^libsql:\/\//, '').replace(/\.turso\.io.*$/, '');
}
3d. 添加 deleteBranch 方法(软删除):
1. 验证目标 project 有 parent_project_id(是 branch)
2. 设置 sys_project: branch_status='deleted', status='archived',
scheduled_delete_at = now + 7天
3. 调用 adapter.deleteDatabase() —— 失败时记录 warning 但不阻止软删除完成
物理清理可由定时任务(或后续实现)处理 scheduled_delete_at 到期的记录。
Step 4 — ObjectQL 定义 (packages/services/service-tenant/src/objects/sys-project.object.ts)
新增字段:
parent_project_id text nullable
branch_name text nullable, maxLength: 100
source_database_name text nullable
branch_status select nullable, options: active/merged/deleted/diverged
forked_at datetime nullable
scheduled_delete_at datetime nullable
新增索引:['parent_project_id']、['parent_project_id', 'branch_status']
Step 5 — REST 路由 (packages/rest/src/branch-routes.ts 新文件)
| Method |
Path |
Handler |
| POST |
/cloud/projects/:projectId/branches |
provisioningService.branchProject(...) |
| GET |
/cloud/projects/:projectId/branches |
query sys_project where parent_project_id = :projectId |
| GET |
/cloud/projects/:projectId/branches/:branchId |
single branch lookup |
| DELETE |
/cloud/projects/:projectId/branches/:branchId |
provisioningService.deleteBranch(...) |
在 rest-api-plugin.ts 的 start 中注册(仿照 registerPackageRoutes 调用方式)。
Step 6 — CLI 命令 (3个新文件)
branch-create.ts
os projects branch-create --project <parentId> --name <slug> [--display-name <text>]
调用 POST 接口,打印创建成功的 branch id、name、databaseUrl。
branch-list.ts
os projects branch-list --project <parentId> [--format table|json]
调用 GET 接口,渲染表格:id | branchName | branchStatus | forkedAt。
branch-delete.ts
os projects branch-delete --project <parentId> --branch <branchId> [--confirm]
调用 DELETE 接口,需 --confirm 防止误删。
实现顺序(依赖关系)
packages/spec — 所有包依赖此处类型
packages/services/service-tenant — adapter + service 逻辑
packages/rest — HTTP 路由(依赖 service)
packages/cli — CLI 命令(依赖 spec 类型 + REST 合约)
验证方案
# 1. 构建
pnpm build
# 2. 单元测试
pnpm test --filter=service-tenant
pnpm test --filter=spec
# 3. 手动验证(需要 TURSO_API_TOKEN 和 TURSO_ORG_NAME 环境变量)
# 创建 branch
os projects branch-create --project <prod-project-id> --name pr-42
# 列出 branches
os projects branch-list --project <prod-project-id>
# 删除 branch
os projects branch-delete --project <prod-project-id> --branch <branch-id> --confirm
# 验证 REST API
curl -X POST http://localhost:3000/cloud/projects/<id>/branches \
-H "Content-Type: application/json" \
-d '{"branchName":"pr-42","createdBy":"user-1"}'
Context
ObjectStack 使用 project-per-database 隔离模型,每个 project 对应一个独立的 Turso SQLite 数据库。用户希望实现类似 Git branch 的功能:从一个 production project fork 出独立的子数据库(branch),用于开发环境、PR 预览、迁移测试等场景。
Turso 原生支持通过
seed参数瞬间 fork 数据库,可直接利用此能力。用户决策:
Critical Files
packages/spec/src/cloud/project.zod.tspackages/services/service-tenant/src/project-provisioning.tspackages/services/service-tenant/src/turso-platform-client.tspackages/services/service-tenant/src/objects/sys-project.object.tspackages/rest/src/branch-routes.tspackages/rest/src/rest-api-plugin.tspackages/cli/src/commands/projects/branch-create.tspackages/cli/src/commands/projects/branch-list.tspackages/cli/src/commands/projects/branch-delete.tsImplementation Steps
Step 1 — Spec Schema (
packages/spec/src/cloud/project.zod.ts)在
ProjectSchema中添加 branch 可选字段:新增独立 schema:
从
packages/spec/src/cloud/index.ts导出新类型。Step 2 — Turso Platform Client (
packages/services/service-tenant/src/turso-platform-client.ts)确认
createDatabase已支持seed参数(代码中已有),若无则补充:Step 3 — 数据库适配器扩展 (
packages/services/service-tenant/src/project-provisioning.ts)3a. 扩展
ProjectDatabaseAdapter接口:3b. 在
TursoProjectDatabaseAdapter中实现forkDatabase:调用
client.createDatabase({ name: target, seed: { type: 'database', name: source } }),再createDatabaseToken,返回 url + token。3c. 添加
branchProject方法(原子性保证):命名工具函数:
3d. 添加
deleteBranch方法(软删除):物理清理可由定时任务(或后续实现)处理
scheduled_delete_at到期的记录。Step 4 — ObjectQL 定义 (
packages/services/service-tenant/src/objects/sys-project.object.ts)新增字段:
新增索引:
['parent_project_id']、['parent_project_id', 'branch_status']Step 5 — REST 路由 (
packages/rest/src/branch-routes.ts新文件)/cloud/projects/:projectId/branchesprovisioningService.branchProject(...)/cloud/projects/:projectId/branches/cloud/projects/:projectId/branches/:branchId/cloud/projects/:projectId/branches/:branchIdprovisioningService.deleteBranch(...)在
rest-api-plugin.ts的start中注册(仿照registerPackageRoutes调用方式)。Step 6 — CLI 命令 (3个新文件)
branch-create.ts调用 POST 接口,打印创建成功的 branch id、name、databaseUrl。
branch-list.ts调用 GET 接口,渲染表格:id | branchName | branchStatus | forkedAt。
branch-delete.ts调用 DELETE 接口,需
--confirm防止误删。实现顺序(依赖关系)
packages/spec— 所有包依赖此处类型packages/services/service-tenant— adapter + service 逻辑packages/rest— HTTP 路由(依赖 service)packages/cli— CLI 命令(依赖 spec 类型 + REST 合约)验证方案