Skip to content

Project Branch Feature — Implementation Plan #1215

@os-zhuang

Description

@os-zhuang

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.tsstart 中注册(仿照 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 防止误删。


实现顺序(依赖关系)

  1. packages/spec — 所有包依赖此处类型
  2. packages/services/service-tenant — adapter + service 逻辑
  3. packages/rest — HTTP 路由(依赖 service)
  4. 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"}'

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions