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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions