Skip to content

Commit 085ae22

Browse files
committed
feat(migrations): handle empty migration directories and add fallback test
1 parent 6b5b66c commit 085ae22

3 files changed

Lines changed: 30 additions & 16 deletions

File tree

docs/pack-theme-schema-design.md

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -118,41 +118,36 @@ packs/<name>/
118118
└── 0001_init.js
119119
```
120120

121-
`migrations/``migration/` 均可(与 `plan.go``migrationFiles()` 一致)。文件内格式为 PB 官方 JS 迁移(见 `vault/pb_migrations/1782707550_updated_users.js` 的写法):
121+
`migrations/``migration/` 均可(与 `plan.go``migrationFiles()` 一致)。文件内格式为 PB 官方 JS 迁移(`migrate(up, down)``plugins/jsvm``registerMigrations` 绑定):
122122

123123
```js
124-
/// <reference path="../pb_data/types.d.ts" />
125124
migrate(
126125
(app) => {
127126
// 幂等守卫:findCollectionByNameOrId 在「找不到」时抛异常(sql: no rows),
128127
// 不会返回 null,所以必须 try/catch。findCollectionsByFilter 未暴露给迁移 VM。
129-
let exists = false;
130128
try {
131129
app.findCollectionByNameOrId("moments");
132-
exists = true;
133-
} catch (e) {
134-
exists = false;
130+
return; // 已存在(幂等)
131+
} catch (_e) {
132+
// 未找到,继续创建
135133
}
136-
if (exists) return;
137134

138135
const collection = new Collection({
139136
/* type/name/fields/rules */
140137
});
141138
return app.save(collection);
142139
},
143140
(app) => {
144-
let collection = null;
145141
try {
146-
collection = app.findCollectionByNameOrId("moments");
147-
} catch (e) {
148-
collection = null;
142+
return app.delete(app.findCollectionByNameOrId("moments"));
143+
} catch (_e) {
144+
return; // 已删除
149145
}
150-
if (collection) return app.delete(collection);
151146
}
152147
);
153148
```
154149

155-
> ⚠️ 实测(`vault/pb_migrations/packmigration_spike_test.go`):`findCollectionByNameOrId` 找不到时抛 `sql: no rows in result set`,不是返回 null;`findCollectionsByFilter` 未暴露给 JS 迁移 VM。所以建表前判断「是否已存在」必须用 `try/catch`
150+
> ⚠️ 实测(`vault/internal/pack/js_migration_test.go`):`findCollectionByNameOrId` 找不到时抛 `sql: no rows in result set`,不是返回 null;`findCollectionsByFilter` 未暴露给 JS 迁移 VM。所以建表前判断「是否已存在」必须用 `try/catch`
156151
157152
### 5.2 迁移文件名必须带数字 ID
158153

@@ -222,7 +217,7 @@ apis.Serve()
222217
- `schema.ts`(Zod 校验)**保持不变**,仍是可选的 record 校验层。
223218
- `StageHooks` / `validation.RegisterWithSources` **保持不变**
224219
- 新增 `StageMigrations``StageHooks` 平行,都在 `jsvm.MustRegister` 之前完成(`jsvm` 在注册时立即读 `MigrationsDir` 加载 JS 迁移)。
225-
- 校验(Zod)与 DDL(PB collection)之间仍可能漂移;短期靠文档约定,长期可考虑用 `/api/vanblog/schema``vault/internal/schema/schema.go`)反推类型生成 Zod,**但这是远期优化,不在本次范围**
220+
- 校验(Zod)与 DDL(PB collection)**两个正交关注点**:DDL 管「建表」,Zod 管「记录契约」(类型 + 客户端校验)。两者漂移是**低严重度的类型漂移**——不影响数据完整性,PB 字段校验兜底;**维持分离设计,不追求 codegen 消除漂移**
226221

227222
---
228223

vault/internal/pack/migrations.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,9 @@ func packMigrationFiles(p Pack) (string, []string, error) {
175175
files = append(files, entry.Name())
176176
}
177177

178+
if len(files) == 0 {
179+
continue // empty dir; try the next one (e.g. migration/)
180+
}
178181
sort.Strings(files)
179182
return dir, files, nil
180183
}

vault/internal/pack/migrations_test.go

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ func TestStageMigrationsNoCollisionAcrossPacks(t *testing.T) {
5252

5353
func TestStageMigrationsRejectsDuplicateIdWithinPack(t *testing.T) {
5454
p := migrationPack("dup", fstest.MapFS{
55-
"pack.json": {Data: []byte(`{"name":"dup","version":"1.0.0"}`)},
55+
"pack.json": {Data: []byte(`{"name":"dup","version":"1.0.0"}`)},
5656
"migrations/0001_a.js": {Data: []byte("a")},
5757
"migrations/0001_b.js": {Data: []byte("b")},
5858
})
@@ -94,6 +94,22 @@ func TestStageMigrationsMissingCoreDirIsSkipped(t *testing.T) {
9494
}
9595
}
9696

97+
func TestStageMigrationsFallsBackWhenMigrationsDirEmpty(t *testing.T) {
98+
p := migrationPack("fallback", fstest.MapFS{
99+
"pack.json": {Data: []byte(`{"name":"fallback","version":"1.0.0"}`)},
100+
"migrations": {Mode: fs.ModeDir},
101+
"migration/0001_init.js": {Data: []byte("migrate((app)=>{})")},
102+
})
103+
destination := filepath.Join(t.TempDir(), "migrations")
104+
if err := StageMigrations("", []Pack{p}, destination); err != nil {
105+
t.Fatal(err)
106+
}
107+
got := diskTree(t, destination)
108+
if got["pack--fallback--0001_init.js"] != "migrate((app)=>{})" {
109+
t.Fatalf("fallback migration not staged: %v", got)
110+
}
111+
}
112+
97113
func TestStageMigrationsRejectsInvalidResources(t *testing.T) {
98114
tests := map[string]fstest.MapFS{
99115
"nested": {"pack.json": {Data: []byte(`{"name":"nested","version":"1.0.0"}`)}, "migrations/sub/0001.js": {Data: []byte("x")}},
@@ -115,7 +131,7 @@ func TestStageMigrationsFailurePreservesOldState(t *testing.T) {
115131
writeFile(t, filepath.Join(destination, "good.js"), "good")
116132

117133
bad := migrationPack("broken", fstest.MapFS{
118-
"pack.json": {Data: []byte(`{"name":"broken","version":"1.0.0"}`)},
134+
"pack.json": {Data: []byte(`{"name":"broken","version":"1.0.0"}`)},
119135
"migrations/0001_a.js": {Data: []byte("a")},
120136
"migrations/0001_b.js": {Data: []byte("b")},
121137
})

0 commit comments

Comments
 (0)