Skip to content

Commit f5f31b4

Browse files
committed
refactor: migration run() and pure v1-to-v2 transform
1 parent a709af0 commit f5f31b4

36 files changed

Lines changed: 991 additions & 829 deletions

apps/web/src/services/storage/migrations/__tests__/v1-to-v2.test.ts

Lines changed: 112 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import { describe, expect, test } from "bun:test";
2-
import { DEFAULT_BACKGROUND_BLUR_INTENSITY } from "@/background/blur";
3-
import { DEFAULT_BACKGROUND_COLOR } from "@/background/color";
4-
import { DEFAULT_CANVAS_SIZE } from "@/canvas/sizes";
5-
const DEFAULT_FPS = 30;
6-
import type { MediaAssetData } from "@/services/storage/types";
7-
import { getProjectId, transformProjectV1ToV2 } from "../transformers/v1-to-v2";
2+
import {
3+
getProjectId,
4+
transformProjectV1ToV2,
5+
type V1ToV2Context,
6+
} from "../transformers/v1-to-v2";
87
import {
98
projectWithNoId,
109
projectWithNullValues,
@@ -13,10 +12,15 @@ import {
1312
v2Project,
1413
} from "./fixtures";
1514

15+
const DEFAULT_FPS = 30;
16+
const DEFAULT_BACKGROUND_BLUR_INTENSITY = 10;
17+
const DEFAULT_BACKGROUND_COLOR = "#000000";
18+
const DEFAULT_CANVAS_SIZE = { width: 1920, height: 1080 };
19+
1620
describe("V1 to V2 Migration", () => {
1721
describe("transformProjectV1ToV2", () => {
18-
test("creates metadata object from flat properties", async () => {
19-
const result = await transformProjectV1ToV2({ project: v1Project });
22+
test("creates metadata object from flat properties", () => {
23+
const result = transformProjectV1ToV2({ project: v1Project });
2024

2125
expect(result.skipped).toBe(false);
2226
expect(result.project.version).toBe(2);
@@ -28,48 +32,48 @@ describe("V1 to V2 Migration", () => {
2832
expect(typeof metadata.updatedAt).toBe("string");
2933
});
3034

31-
test("creates settings object from flat properties", async () => {
32-
const result = await transformProjectV1ToV2({ project: v1Project });
35+
test("creates settings object from flat properties", () => {
36+
const result = transformProjectV1ToV2({ project: v1Project });
3337

3438
const settings = result.project.settings as Record<string, unknown>;
3539
expect(settings.fps).toBe(v1Project.fps);
3640
expect(settings.canvasSize).toEqual(v1Project.canvasSize);
3741
expect(settings.originalCanvasSize).toBe(null);
3842
});
3943

40-
test("converts color background correctly", async () => {
41-
const result = await transformProjectV1ToV2({ project: v1Project });
44+
test("converts color background correctly", () => {
45+
const result = transformProjectV1ToV2({ project: v1Project });
4246

4347
const settings = result.project.settings as Record<string, unknown>;
4448
const background = settings.background as Record<string, unknown>;
4549
expect(background.type).toBe("color");
4650
expect(background.color).toBe(v1Project.backgroundColor);
4751
});
4852

49-
test("converts blur background correctly", async () => {
53+
test("converts blur background correctly", () => {
5054
const projectWithBlur = {
5155
...v1Project,
5256
backgroundType: "blur",
5357
blurIntensity: 30,
5458
};
55-
const result = await transformProjectV1ToV2({ project: projectWithBlur });
59+
const result = transformProjectV1ToV2({ project: projectWithBlur });
5660

5761
const settings = result.project.settings as Record<string, unknown>;
5862
const background = settings.background as Record<string, unknown>;
5963
expect(background.type).toBe("blur");
6064
expect(background.blurIntensity).toBe(30);
6165
});
6266

63-
test("applies legacy bookmarks to main scene", async () => {
64-
const result = await transformProjectV1ToV2({ project: v1Project });
67+
test("applies legacy bookmarks to main scene", () => {
68+
const result = transformProjectV1ToV2({ project: v1Project });
6569

6670
const scenes = result.project.scenes as Array<Record<string, unknown>>;
6771
const mainScene = scenes.find((s) => s.isMain === true);
6872
expect(mainScene?.bookmarks).toEqual(v1Project.bookmarks);
6973
});
7074

71-
test("preserves existing scene bookmarks", async () => {
72-
const result = await transformProjectV1ToV2({
75+
test("preserves existing scene bookmarks", () => {
76+
const result = transformProjectV1ToV2({
7377
project: v1ProjectWithMultipleScenes,
7478
});
7579

@@ -78,22 +82,22 @@ describe("V1 to V2 Migration", () => {
7882
expect(introScene?.bookmarks).toEqual([1.0]);
7983
});
8084

81-
test("skips project that already has v2 structure", async () => {
82-
const result = await transformProjectV1ToV2({ project: v2Project });
85+
test("skips project that already has v2 structure", () => {
86+
const result = transformProjectV1ToV2({ project: v2Project });
8387

8488
expect(result.skipped).toBe(true);
8589
expect(result.reason).toBe("already v2");
8690
});
8791

88-
test("skips project with no id", async () => {
89-
const result = await transformProjectV1ToV2({ project: projectWithNoId });
92+
test("skips project with no id", () => {
93+
const result = transformProjectV1ToV2({ project: projectWithNoId });
9094

9195
expect(result.skipped).toBe(true);
9296
expect(result.reason).toBe("no project id");
9397
});
9498

95-
test("handles null values gracefully", async () => {
96-
const result = await transformProjectV1ToV2({
99+
test("handles null values gracefully", () => {
100+
const result = transformProjectV1ToV2({
97101
project: projectWithNullValues,
98102
});
99103

@@ -103,13 +107,13 @@ describe("V1 to V2 Migration", () => {
103107
expect(settings.canvasSize).toEqual(DEFAULT_CANVAS_SIZE);
104108
});
105109

106-
test("uses default values for missing properties", async () => {
110+
test("uses default values for missing properties", () => {
107111
const minimalProject = {
108112
id: "minimal",
109113
version: 1,
110114
scenes: [],
111115
};
112-
const result = await transformProjectV1ToV2({ project: minimalProject });
116+
const result = transformProjectV1ToV2({ project: minimalProject });
113117

114118
const settings = result.project.settings as Record<string, unknown>;
115119
expect(settings.fps).toBe(DEFAULT_FPS);
@@ -120,14 +124,14 @@ describe("V1 to V2 Migration", () => {
120124
expect(background.color).toBe(DEFAULT_BACKGROUND_COLOR);
121125
});
122126

123-
test("uses default blur intensity when missing", async () => {
127+
test("uses default blur intensity when missing", () => {
124128
const projectWithBlurNoIntensity = {
125129
id: "blur-no-intensity",
126130
version: 1,
127131
backgroundType: "blur",
128132
scenes: [],
129133
};
130-
const result = await transformProjectV1ToV2({
134+
const result = transformProjectV1ToV2({
131135
project: projectWithBlurNoIntensity,
132136
});
133137

@@ -136,23 +140,23 @@ describe("V1 to V2 Migration", () => {
136140
expect(background.blurIntensity).toBe(DEFAULT_BACKGROUND_BLUR_INTENSITY);
137141
});
138142

139-
test("preserves currentSceneId", async () => {
140-
const result = await transformProjectV1ToV2({ project: v1Project });
143+
test("preserves currentSceneId", () => {
144+
const result = transformProjectV1ToV2({ project: v1Project });
141145
expect(result.project.currentSceneId).toBe(v1Project.currentSceneId);
142146
});
143147

144-
test("finds main scene id when currentSceneId missing", async () => {
148+
test("finds main scene id when currentSceneId missing", () => {
145149
const projectWithoutCurrentScene = {
146150
...v1Project,
147151
currentSceneId: undefined,
148152
};
149-
const result = await transformProjectV1ToV2({
153+
const result = transformProjectV1ToV2({
150154
project: projectWithoutCurrentScene,
151155
});
152156
expect(result.project.currentSceneId).toBe("scene-main");
153157
});
154158

155-
test("skips loading tracks if scene already has tracks", async () => {
159+
test("skips loading tracks if scene already has tracks", () => {
156160
const projectWithTracks = {
157161
...v1Project,
158162
scenes: [
@@ -175,7 +179,7 @@ describe("V1 to V2 Migration", () => {
175179
],
176180
};
177181

178-
const result = await transformProjectV1ToV2({
182+
const result = transformProjectV1ToV2({
179183
project: projectWithTracks,
180184
});
181185

@@ -188,22 +192,32 @@ describe("V1 to V2 Migration", () => {
188192
});
189193

190194
describe("Track Loading and Transformation", () => {
191-
test("loads tracks from legacy DB and transforms media track to video track", async () => {
192-
const mockLoadMediaAsset = async ({
193-
mediaId,
194-
}: {
195-
mediaId: string;
196-
}): Promise<MediaAssetData | null> => {
197-
if (mediaId === "media-1") {
198-
return {
199-
id: "media-1",
200-
name: "Test Video",
201-
type: "video",
202-
size: 1000,
203-
lastModified: Date.now(),
204-
};
205-
}
206-
return null;
195+
test("loads tracks from legacy DB and transforms media track to video track", () => {
196+
const context: V1ToV2Context = {
197+
legacyTracksBySceneId: {
198+
"scene-main": [
199+
{
200+
id: "legacy-track-1",
201+
type: "media",
202+
name: "Legacy media track",
203+
elements: [
204+
{
205+
id: "media-element-1",
206+
name: "Test video clip",
207+
type: "media",
208+
mediaId: "media-1",
209+
duration: 120,
210+
startTime: 0,
211+
trimStart: 0,
212+
trimEnd: 0,
213+
},
214+
],
215+
},
216+
],
217+
},
218+
mediaTypesById: {
219+
"media-1": "video",
220+
},
207221
};
208222

209223
const projectWithLegacyTracks = {
@@ -221,19 +235,50 @@ describe("V1 to V2 Migration", () => {
221235
],
222236
};
223237

224-
// mock IndexedDB for this test would require setting up a test environment
225-
// for now, we test that the transformer handles empty tracks gracefully
226-
const result = await transformProjectV1ToV2({
238+
const result = transformProjectV1ToV2({
227239
project: projectWithLegacyTracks,
228-
options: { loadMediaAsset: mockLoadMediaAsset },
240+
context,
229241
});
230242

231243
const scenes = result.project.scenes as Array<Record<string, unknown>>;
232244
const mainScene = scenes[0];
233-
expect(Array.isArray(mainScene.tracks)).toBe(true);
245+
const tracks = mainScene.tracks as Array<Record<string, unknown>>;
246+
expect(Array.isArray(tracks)).toBe(true);
247+
expect(tracks).toHaveLength(1);
248+
expect(tracks[0].type).toBe("video");
249+
expect(tracks[0].isMain).toBe(true);
234250
});
235251

236-
test("transforms text element preserving opacity and migrating position", async () => {
252+
test("transforms text element preserving opacity and migrating position", () => {
253+
const context: V1ToV2Context = {
254+
legacyTracksBySceneId: {
255+
"scene-1": [
256+
{
257+
id: "legacy-text-track",
258+
type: "text",
259+
name: "Text",
260+
elements: [
261+
{
262+
id: "text-element-1",
263+
name: "Title",
264+
type: "text",
265+
content: "Hello",
266+
x: 120,
267+
y: 240,
268+
rotation: 15,
269+
opacity: 0.5,
270+
duration: 90,
271+
startTime: 10,
272+
trimStart: 0,
273+
trimEnd: 0,
274+
},
275+
],
276+
},
277+
],
278+
},
279+
mediaTypesById: {},
280+
};
281+
237282
const projectWithTextTrack = {
238283
id: "project-text",
239284
version: 1,
@@ -251,15 +296,22 @@ describe("V1 to V2 Migration", () => {
251296
],
252297
};
253298

254-
// since tracks are empty, transformation won't happen
255-
// but we verify the structure is correct
256-
const result = await transformProjectV1ToV2({
299+
const result = transformProjectV1ToV2({
257300
project: projectWithTextTrack,
301+
context,
258302
});
259303

260304
expect(result.skipped).toBe(false);
261305
const scenes = result.project.scenes as Array<Record<string, unknown>>;
262-
expect(scenes.length).toBe(1);
306+
const tracks = scenes[0].tracks as Array<Record<string, unknown>>;
307+
const elements = tracks[0].elements as Array<Record<string, unknown>>;
308+
const textElement = elements[0];
309+
expect(textElement.opacity).toBe(0.5);
310+
expect(textElement.transform).toEqual({
311+
scale: 1,
312+
position: { x: 120, y: 240 },
313+
rotate: 15,
314+
});
263315
});
264316
});
265317

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
1-
import type { MigrationResult, ProjectRecord } from "./transformers/types";
2-
3-
export abstract class StorageMigration {
4-
abstract from: number;
5-
abstract to: number;
6-
abstract transform(
7-
project: ProjectRecord,
8-
): Promise<MigrationResult<ProjectRecord>>;
9-
}
1+
import type { MigrationResult, ProjectRecord } from "./transformers/types";
2+
3+
export interface StorageMigrationRunArgs {
4+
projectId: string;
5+
project: ProjectRecord;
6+
}
7+
8+
export abstract class StorageMigration {
9+
abstract from: number;
10+
abstract to: number;
11+
12+
abstract run({
13+
projectId,
14+
project,
15+
}: StorageMigrationRunArgs): Promise<MigrationResult<ProjectRecord>>;
16+
}

0 commit comments

Comments
 (0)