summaryrefslogtreecommitdiff
path: root/apps/web/src/services/storage/migrations
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/src/services/storage/migrations')
-rw-r--r--apps/web/src/services/storage/migrations/__tests__/helpers.ts39
-rw-r--r--apps/web/src/services/storage/migrations/__tests__/v0-to-v1.test.ts7
-rw-r--r--apps/web/src/services/storage/migrations/__tests__/v1-to-v2.test.ts41
-rw-r--r--apps/web/src/services/storage/migrations/__tests__/v15-to-v16.test.ts9
-rw-r--r--apps/web/src/services/storage/migrations/__tests__/v16-to-v17.test.ts13
-rw-r--r--apps/web/src/services/storage/migrations/__tests__/v18-to-v19.test.ts17
-rw-r--r--apps/web/src/services/storage/migrations/__tests__/v19-to-v20.test.ts21
-rw-r--r--apps/web/src/services/storage/migrations/__tests__/v2-to-v3.test.ts17
-rw-r--r--apps/web/src/services/storage/migrations/__tests__/v20-to-v21.test.ts11
-rw-r--r--apps/web/src/services/storage/migrations/__tests__/v21-to-v22.test.ts13
-rw-r--r--apps/web/src/services/storage/migrations/__tests__/v22-to-v23.test.ts20
-rw-r--r--apps/web/src/services/storage/migrations/__tests__/v26-to-v27.test.ts15
-rw-r--r--apps/web/src/services/storage/migrations/__tests__/v27-to-v28.test.ts37
-rw-r--r--apps/web/src/services/storage/migrations/__tests__/v3-to-v4.test.ts25
-rw-r--r--apps/web/src/services/storage/migrations/__tests__/v4-to-v5.test.ts25
-rw-r--r--apps/web/src/services/storage/migrations/__tests__/v5-to-v6.test.ts17
-rw-r--r--apps/web/src/services/storage/migrations/__tests__/v8-to-v9.test.ts26
-rw-r--r--apps/web/src/services/storage/migrations/transformers/v1-to-v2.ts104
-rw-r--r--apps/web/src/services/storage/migrations/transformers/v13-to-v14.ts5
-rw-r--r--apps/web/src/services/storage/migrations/transformers/v26-to-v27.ts17
-rw-r--r--apps/web/src/services/storage/migrations/transformers/v3-to-v4.ts15
21 files changed, 243 insertions, 251 deletions
diff --git a/apps/web/src/services/storage/migrations/__tests__/helpers.ts b/apps/web/src/services/storage/migrations/__tests__/helpers.ts
new file mode 100644
index 00000000..381cb935
--- /dev/null
+++ b/apps/web/src/services/storage/migrations/__tests__/helpers.ts
@@ -0,0 +1,39 @@
+/**
+ * Type-safe accessors for navigating migration output.
+ *
+ * Migrations input/output `Record<string, unknown>` because they handle
+ * potentially malformed data from older versions. Tests need to inspect
+ * specific properties of the migrated result without using type assertions.
+ *
+ * These helpers narrow through type guards (Array.isArray + a record
+ * predicate), so the file contains no unsafe assertions despite turning
+ * unknown into concrete shapes.
+ */
+
+function describe(value: unknown): string {
+ if (value === null) return "null";
+ if (Array.isArray(value)) return "array";
+ return typeof value;
+}
+
+function isRecord(value: unknown): value is Record<string, unknown> {
+ return typeof value === "object" && value !== null && !Array.isArray(value);
+}
+
+export function asRecord(value: unknown): Record<string, unknown> {
+ if (!isRecord(value)) {
+ throw new Error(`Expected record, got ${describe(value)}`);
+ }
+ return value;
+}
+
+export function asArray(value: unknown): unknown[] {
+ if (!Array.isArray(value)) {
+ throw new Error(`Expected array, got ${describe(value)}`);
+ }
+ return value;
+}
+
+export function asRecordArray(value: unknown): Record<string, unknown>[] {
+ return asArray(value).map(asRecord);
+}
diff --git a/apps/web/src/services/storage/migrations/__tests__/v0-to-v1.test.ts b/apps/web/src/services/storage/migrations/__tests__/v0-to-v1.test.ts
index 2cd67da8..73b64fed 100644
--- a/apps/web/src/services/storage/migrations/__tests__/v0-to-v1.test.ts
+++ b/apps/web/src/services/storage/migrations/__tests__/v0-to-v1.test.ts
@@ -8,6 +8,7 @@ import {
v0ProjectWithMetadata,
v1Project,
} from "./fixtures";
+import { asArray, asRecord, asRecordArray } from "./helpers";
describe("V0 to V1 Migration", () => {
const fixedDate = new Date("2024-06-01T12:00:00.000Z");
@@ -22,7 +23,7 @@ describe("V0 to V1 Migration", () => {
expect(result.skipped).toBe(false);
expect(result.project.version).toBe(1);
expect(Array.isArray(result.project.scenes)).toBe(true);
- expect((result.project.scenes as unknown[]).length).toBe(1);
+ expect(asArray(result.project.scenes).length).toBe(1);
expect(result.project.currentSceneId).toBeDefined();
});
@@ -32,7 +33,7 @@ describe("V0 to V1 Migration", () => {
options: { now: fixedDate },
});
- const scenes = result.project.scenes as Array<Record<string, unknown>>;
+ const scenes = asRecordArray(result.project.scenes);
const mainScene = scenes[0];
expect(mainScene.isMain).toBe(true);
@@ -48,7 +49,7 @@ describe("V0 to V1 Migration", () => {
options: { now: fixedDate },
});
- const metadata = result.project.metadata as Record<string, unknown>;
+ const metadata = asRecord(result.project.metadata);
expect(metadata.updatedAt).toBe(fixedDate.toISOString());
});
diff --git a/apps/web/src/services/storage/migrations/__tests__/v1-to-v2.test.ts b/apps/web/src/services/storage/migrations/__tests__/v1-to-v2.test.ts
index 0840ac76..6670c479 100644
--- a/apps/web/src/services/storage/migrations/__tests__/v1-to-v2.test.ts
+++ b/apps/web/src/services/storage/migrations/__tests__/v1-to-v2.test.ts
@@ -11,6 +11,7 @@ import {
v1ProjectWithMultipleScenes,
v2Project,
} from "./fixtures";
+import { asRecord, asRecordArray } from "./helpers";
const DEFAULT_FPS = 30;
const DEFAULT_BACKGROUND_BLUR_INTENSITY = 10;
@@ -25,7 +26,7 @@ describe("V1 to V2 Migration", () => {
expect(result.skipped).toBe(false);
expect(result.project.version).toBe(2);
- const metadata = result.project.metadata as Record<string, unknown>;
+ const metadata = asRecord(result.project.metadata);
expect(metadata.id).toBe(v1Project.id);
expect(metadata.name).toBe(v1Project.name);
expect(typeof metadata.createdAt).toBe("string");
@@ -35,7 +36,7 @@ describe("V1 to V2 Migration", () => {
test("creates settings object from flat properties", () => {
const result = transformProjectV1ToV2({ project: v1Project });
- const settings = result.project.settings as Record<string, unknown>;
+ const settings = asRecord(result.project.settings);
expect(settings.fps).toBe(v1Project.fps);
expect(settings.canvasSize).toEqual(v1Project.canvasSize);
expect(settings.originalCanvasSize).toBe(null);
@@ -44,8 +45,8 @@ describe("V1 to V2 Migration", () => {
test("converts color background correctly", () => {
const result = transformProjectV1ToV2({ project: v1Project });
- const settings = result.project.settings as Record<string, unknown>;
- const background = settings.background as Record<string, unknown>;
+ const settings = asRecord(result.project.settings);
+ const background = asRecord(settings.background);
expect(background.type).toBe("color");
expect(background.color).toBe(v1Project.backgroundColor);
});
@@ -58,8 +59,8 @@ describe("V1 to V2 Migration", () => {
};
const result = transformProjectV1ToV2({ project: projectWithBlur });
- const settings = result.project.settings as Record<string, unknown>;
- const background = settings.background as Record<string, unknown>;
+ const settings = asRecord(result.project.settings);
+ const background = asRecord(settings.background);
expect(background.type).toBe("blur");
expect(background.blurIntensity).toBe(30);
});
@@ -67,7 +68,7 @@ describe("V1 to V2 Migration", () => {
test("applies legacy bookmarks to main scene", () => {
const result = transformProjectV1ToV2({ project: v1Project });
- const scenes = result.project.scenes as Array<Record<string, unknown>>;
+ const scenes = asRecordArray(result.project.scenes);
const mainScene = scenes.find((s) => s.isMain === true);
expect(mainScene?.bookmarks).toEqual(v1Project.bookmarks);
});
@@ -77,7 +78,7 @@ describe("V1 to V2 Migration", () => {
project: v1ProjectWithMultipleScenes,
});
- const scenes = result.project.scenes as Array<Record<string, unknown>>;
+ const scenes = asRecordArray(result.project.scenes);
const introScene = scenes.find((s) => s.name === "Intro");
expect(introScene?.bookmarks).toEqual([1.0]);
});
@@ -102,7 +103,7 @@ describe("V1 to V2 Migration", () => {
});
expect(result.skipped).toBe(false);
- const settings = result.project.settings as Record<string, unknown>;
+ const settings = asRecord(result.project.settings);
expect(settings.fps).toBe(DEFAULT_FPS);
expect(settings.canvasSize).toEqual(DEFAULT_CANVAS_SIZE);
});
@@ -115,11 +116,11 @@ describe("V1 to V2 Migration", () => {
};
const result = transformProjectV1ToV2({ project: minimalProject });
- const settings = result.project.settings as Record<string, unknown>;
+ const settings = asRecord(result.project.settings);
expect(settings.fps).toBe(DEFAULT_FPS);
expect(settings.canvasSize).toEqual(DEFAULT_CANVAS_SIZE);
- const background = settings.background as Record<string, unknown>;
+ const background = asRecord(settings.background);
expect(background.type).toBe("color");
expect(background.color).toBe(DEFAULT_BACKGROUND_COLOR);
});
@@ -135,8 +136,8 @@ describe("V1 to V2 Migration", () => {
project: projectWithBlurNoIntensity,
});
- const settings = result.project.settings as Record<string, unknown>;
- const background = settings.background as Record<string, unknown>;
+ const settings = asRecord(result.project.settings);
+ const background = asRecord(settings.background);
expect(background.blurIntensity).toBe(DEFAULT_BACKGROUND_BLUR_INTENSITY);
});
@@ -183,9 +184,9 @@ describe("V1 to V2 Migration", () => {
project: projectWithTracks,
});
- const scenes = result.project.scenes as Array<Record<string, unknown>>;
+ const scenes = asRecordArray(result.project.scenes);
const mainScene = scenes[0];
- const tracks = mainScene.tracks as Array<Record<string, unknown>>;
+ const tracks = asRecordArray(mainScene.tracks);
expect(tracks.length).toBe(1);
expect(tracks[0].name).toBe("Existing Track");
});
@@ -240,9 +241,9 @@ describe("V1 to V2 Migration", () => {
context,
});
- const scenes = result.project.scenes as Array<Record<string, unknown>>;
+ const scenes = asRecordArray(result.project.scenes);
const mainScene = scenes[0];
- const tracks = mainScene.tracks as Array<Record<string, unknown>>;
+ const tracks = asRecordArray(mainScene.tracks);
expect(Array.isArray(tracks)).toBe(true);
expect(tracks).toHaveLength(1);
expect(tracks[0].type).toBe("video");
@@ -302,9 +303,9 @@ describe("V1 to V2 Migration", () => {
});
expect(result.skipped).toBe(false);
- const scenes = result.project.scenes as Array<Record<string, unknown>>;
- const tracks = scenes[0].tracks as Array<Record<string, unknown>>;
- const elements = tracks[0].elements as Array<Record<string, unknown>>;
+ const scenes = asRecordArray(result.project.scenes);
+ const tracks = asRecordArray(scenes[0].tracks);
+ const elements = asRecordArray(tracks[0].elements);
const textElement = elements[0];
expect(textElement.opacity).toBe(0.5);
expect(textElement.transform).toEqual({
diff --git a/apps/web/src/services/storage/migrations/__tests__/v15-to-v16.test.ts b/apps/web/src/services/storage/migrations/__tests__/v15-to-v16.test.ts
index 122af2cb..1af445ac 100644
--- a/apps/web/src/services/storage/migrations/__tests__/v15-to-v16.test.ts
+++ b/apps/web/src/services/storage/migrations/__tests__/v15-to-v16.test.ts
@@ -1,5 +1,6 @@
import { describe, expect, test } from "bun:test";
import { transformProjectV15ToV16 } from "../transformers/v15-to-v16";
+import { asRecordArray } from "./helpers";
describe("V15 to V16 Migration", () => {
test("renames sticker tracks to graphic tracks", () => {
@@ -61,10 +62,10 @@ describe("V15 to V16 Migration", () => {
expect(result.skipped).toBe(false);
expect(result.project.version).toBe(16);
- expect(
- (result.project.scenes as Array<{ tracks: Array<{ type: string }> }>)[0]
- .tracks[0].type,
- ).toBe("graphic");
+ const firstTrack = asRecordArray(
+ asRecordArray(result.project.scenes)[0].tracks,
+ )[0];
+ expect(firstTrack.type).toBe("graphic");
});
test("skips projects already on v16", () => {
diff --git a/apps/web/src/services/storage/migrations/__tests__/v16-to-v17.test.ts b/apps/web/src/services/storage/migrations/__tests__/v16-to-v17.test.ts
index 352ca762..10a792ca 100644
--- a/apps/web/src/services/storage/migrations/__tests__/v16-to-v17.test.ts
+++ b/apps/web/src/services/storage/migrations/__tests__/v16-to-v17.test.ts
@@ -1,5 +1,6 @@
import { describe, expect, test } from "bun:test";
import { transformProjectV16ToV17 } from "../transformers/v16-to-v17";
+import { asRecord, asRecordArray } from "./helpers";
describe("V16 to V17 Migration", () => {
test("adds center stroke alignment to masks that do not have it", () => {
@@ -97,13 +98,13 @@ describe("V16 to V17 Migration", () => {
expect(result.skipped).toBe(false);
expect(result.project.version).toBe(17);
- const migratedMasks = (
- ((result.project.scenes as Array<{ tracks: Array<{ elements: Array<{ masks: Array<{ params: Record<string, unknown> }> }> }> }>)[0]
- .tracks[0].elements[0].masks)
- );
+ const firstScene = asRecordArray(result.project.scenes)[0];
+ const firstTrack = asRecordArray(firstScene.tracks)[0];
+ const firstElement = asRecordArray(firstTrack.elements)[0];
+ const migratedMasks = asRecordArray(firstElement.masks);
- expect(migratedMasks[0].params.strokeAlign).toBe("center");
- expect(migratedMasks[1].params.strokeAlign).toBe("outside");
+ expect(asRecord(migratedMasks[0].params).strokeAlign).toBe("center");
+ expect(asRecord(migratedMasks[1].params).strokeAlign).toBe("outside");
});
test("skips projects already on v17", () => {
diff --git a/apps/web/src/services/storage/migrations/__tests__/v18-to-v19.test.ts b/apps/web/src/services/storage/migrations/__tests__/v18-to-v19.test.ts
index 0f679b6d..0cb30f2b 100644
--- a/apps/web/src/services/storage/migrations/__tests__/v18-to-v19.test.ts
+++ b/apps/web/src/services/storage/migrations/__tests__/v18-to-v19.test.ts
@@ -1,5 +1,6 @@
import { describe, expect, test } from "bun:test";
import { transformProjectV18ToV19 } from "../transformers/v18-to-v19";
+import { asRecord } from "./helpers";
describe("V18 to V19 Migration", () => {
test("adds canvas size mode and empty remembered custom size defaults", () => {
@@ -26,15 +27,13 @@ describe("V18 to V19 Migration", () => {
expect(result.skipped).toBe(false);
expect(result.project.version).toBe(19);
- expect(
- (result.project.settings as Record<string, unknown>).canvasSizeMode,
- ).toBe("preset");
- expect(
- (result.project.settings as Record<string, unknown>).lastCustomCanvasSize,
- ).toBeNull();
- expect(
- (result.project.settings as Record<string, unknown>).originalCanvasSize,
- ).toEqual({ width: 1920, height: 1080 });
+ const settings = asRecord(result.project.settings);
+ expect(settings.canvasSizeMode).toBe("preset");
+ expect(settings.lastCustomCanvasSize).toBeNull();
+ expect(settings.originalCanvasSize).toEqual({
+ width: 1920,
+ height: 1080,
+ });
});
test("skips projects already on v19", () => {
diff --git a/apps/web/src/services/storage/migrations/__tests__/v19-to-v20.test.ts b/apps/web/src/services/storage/migrations/__tests__/v19-to-v20.test.ts
index 0e0cfbbe..b1bfe86f 100644
--- a/apps/web/src/services/storage/migrations/__tests__/v19-to-v20.test.ts
+++ b/apps/web/src/services/storage/migrations/__tests__/v19-to-v20.test.ts
@@ -1,5 +1,6 @@
import { describe, expect, test } from "bun:test";
import { transformProjectV19ToV20 } from "../transformers/v19-to-v20";
+import { asRecordArray } from "./helpers";
describe("V19 to V20 Migration", () => {
test("backfills source audio enabled state on video elements", () => {
@@ -82,19 +83,16 @@ describe("V19 to V20 Migration", () => {
expect(result.skipped).toBe(false);
expect(result.project.version).toBe(20);
- expect(
- ((result.project.scenes as Array<Record<string, unknown>>)[0]
- .tracks as Array<Record<string, unknown>>)[0].elements,
- ).toEqual([
+ const scene = asRecordArray(result.project.scenes)[0];
+ const tracks = asRecordArray(scene.tracks);
+ expect(tracks[0].elements).toEqual([
expect.objectContaining({
id: "video-1",
isSourceAudioEnabled: true,
}),
]);
expect(
- (((result.project.scenes as Array<Record<string, unknown>>)[0]
- .tracks as Array<Record<string, unknown>>)[1]
- .elements as Array<Record<string, unknown>>)[0].isSourceAudioEnabled,
+ asRecordArray(tracks[1].elements)[0].isSourceAudioEnabled,
).toBeUndefined();
});
@@ -120,11 +118,10 @@ describe("V19 to V20 Migration", () => {
},
});
- expect(
- (((result.project.scenes as Array<Record<string, unknown>>)[0]
- .tracks as Array<Record<string, unknown>>)[0]
- .elements as Array<Record<string, unknown>>)[0].isSourceAudioEnabled,
- ).toBe(false);
+ const scene = asRecordArray(result.project.scenes)[0];
+ const track = asRecordArray(scene.tracks)[0];
+ const element = asRecordArray(track.elements)[0];
+ expect(element.isSourceAudioEnabled).toBe(false);
});
test("skips projects already on v20", () => {
diff --git a/apps/web/src/services/storage/migrations/__tests__/v2-to-v3.test.ts b/apps/web/src/services/storage/migrations/__tests__/v2-to-v3.test.ts
index ba8b3bfb..d18b4dd4 100644
--- a/apps/web/src/services/storage/migrations/__tests__/v2-to-v3.test.ts
+++ b/apps/web/src/services/storage/migrations/__tests__/v2-to-v3.test.ts
@@ -8,6 +8,7 @@ import {
v2ProjectWithBlurBackground,
v3Project,
} from "./fixtures";
+import { asRecord } from "./helpers";
describe("V2 to V3 Migration", () => {
describe("transformProjectV2ToV3", () => {
@@ -17,14 +18,14 @@ describe("V2 to V3 Migration", () => {
expect(result.skipped).toBe(false);
expect(result.project.version).toBe(3);
- const metadata = result.project.metadata as Record<string, unknown>;
+ const metadata = asRecord(result.project.metadata);
expect(typeof metadata.duration).toBe("number");
});
test("calculates duration from scene tracks", () => {
const result = transformProjectV2ToV3({ project: v2Project });
- const metadata = result.project.metadata as Record<string, unknown>;
+ const metadata = asRecord(result.project.metadata);
// v2Project has a video element with duration 15.5 and a text element at startTime 2 with duration 5
// Total duration should be max(15.5, 2+5) = 15.5
expect(metadata.duration).toBe(15.5);
@@ -36,7 +37,7 @@ describe("V2 to V3 Migration", () => {
});
expect(result.skipped).toBe(false);
- const metadata = result.project.metadata as Record<string, unknown>;
+ const metadata = asRecord(result.project.metadata);
// v2ProjectWithBlurBackground has a video with duration 30
expect(metadata.duration).toBe(30);
});
@@ -45,7 +46,7 @@ describe("V2 to V3 Migration", () => {
const result = transformProjectV2ToV3({ project: v2ProjectEmptyScenes });
expect(result.skipped).toBe(false);
- const metadata = result.project.metadata as Record<string, unknown>;
+ const metadata = asRecord(result.project.metadata);
expect(metadata.duration).toBe(0);
});
@@ -55,7 +56,7 @@ describe("V2 to V3 Migration", () => {
});
expect(result.skipped).toBe(false);
- const metadata = result.project.metadata as Record<string, unknown>;
+ const metadata = asRecord(result.project.metadata);
expect(metadata.duration).toBe(0);
});
@@ -91,7 +92,7 @@ describe("V2 to V3 Migration", () => {
test("preserves existing metadata fields", () => {
const result = transformProjectV2ToV3({ project: v2Project });
- const metadata = result.project.metadata as Record<string, unknown>;
+ const metadata = asRecord(result.project.metadata);
expect(metadata.id).toBe(v2Project.metadata.id);
expect(metadata.name).toBe(v2Project.metadata.name);
expect(metadata.thumbnail).toBe(v2Project.metadata.thumbnail);
@@ -122,7 +123,7 @@ describe("V2 to V3 Migration", () => {
});
expect(result.skipped).toBe(false);
- const metadata = result.project.metadata as Record<string, unknown>;
+ const metadata = asRecord(result.project.metadata);
expect(metadata.duration).toBe(0);
});
@@ -156,7 +157,7 @@ describe("V2 to V3 Migration", () => {
};
const result = transformProjectV2ToV3({ project: multiSceneProject });
- const metadata = result.project.metadata as Record<string, unknown>;
+ const metadata = asRecord(result.project.metadata);
// Duration is from main scene only, not sum of all scenes
expect(metadata.duration).toBe(10);
});
diff --git a/apps/web/src/services/storage/migrations/__tests__/v20-to-v21.test.ts b/apps/web/src/services/storage/migrations/__tests__/v20-to-v21.test.ts
index 3d77a775..2737bfc3 100644
--- a/apps/web/src/services/storage/migrations/__tests__/v20-to-v21.test.ts
+++ b/apps/web/src/services/storage/migrations/__tests__/v20-to-v21.test.ts
@@ -1,5 +1,6 @@
import { describe, expect, test } from "bun:test";
import { transformProjectV20ToV21 } from "../transformers/v20-to-v21";
+import { asRecord } from "./helpers";
const LEGACY_DEFAULT_BACKGROUND_BLUR_INTENSITY = 50;
@@ -27,8 +28,8 @@ describe("V20 to V21 Migration", () => {
expect(result.skipped).toBe(false);
expect(result.project.version).toBe(21);
- const settings = result.project.settings as Record<string, unknown>;
- const background = settings.background as Record<string, unknown>;
+ const settings = asRecord(result.project.settings);
+ const background = asRecord(settings.background);
expect(background.blurIntensity).toBe(500);
});
@@ -45,8 +46,8 @@ describe("V20 to V21 Migration", () => {
});
expect(result.skipped).toBe(false);
- const settings = result.project.settings as Record<string, unknown>;
- const background = settings.background as Record<string, unknown>;
+ const settings = asRecord(result.project.settings);
+ const background = asRecord(settings.background);
expect(background.blurIntensity).toBe(
LEGACY_DEFAULT_BACKGROUND_BLUR_INTENSITY,
);
@@ -65,7 +66,7 @@ describe("V20 to V21 Migration", () => {
});
expect(result.skipped).toBe(false);
- const settings = result.project.settings as Record<string, unknown>;
+ const settings = asRecord(result.project.settings);
expect(settings.background).toEqual({ type: "color", color: "#000000" });
});
diff --git a/apps/web/src/services/storage/migrations/__tests__/v21-to-v22.test.ts b/apps/web/src/services/storage/migrations/__tests__/v21-to-v22.test.ts
index 18b815b0..76d9c0e1 100644
--- a/apps/web/src/services/storage/migrations/__tests__/v21-to-v22.test.ts
+++ b/apps/web/src/services/storage/migrations/__tests__/v21-to-v22.test.ts
@@ -1,5 +1,6 @@
import { describe, expect, test } from "bun:test";
import { transformProjectV21ToV22 } from "../transformers/v21-to-v22";
+import { asRecord, asRecordArray } from "./helpers";
describe("V21 to V22 Migration", () => {
test("migrates legacy animation channels to bindings and component channels", () => {
@@ -77,12 +78,12 @@ describe("V21 to V22 Migration", () => {
expect(result.skipped).toBe(false);
expect(result.project.version).toBe(22);
- const scenes = result.project.scenes as Array<Record<string, unknown>>;
- const tracks = scenes[0].tracks as Array<Record<string, unknown>>;
- const elements = tracks[0].elements as Array<Record<string, unknown>>;
- const animations = elements[0].animations as Record<string, unknown>;
- const bindings = animations.bindings as Record<string, Record<string, unknown>>;
- const channels = animations.channels as Record<string, Record<string, unknown>>;
+ const scenes = asRecordArray(result.project.scenes);
+ const tracks = asRecordArray(scenes[0].tracks);
+ const elements = asRecordArray(tracks[0].elements);
+ const animations = asRecord(elements[0].animations);
+ const bindings = asRecord(animations.bindings);
+ const channels = asRecord(animations.channels);
expect(bindings.opacity).toEqual({
path: "opacity",
diff --git a/apps/web/src/services/storage/migrations/__tests__/v22-to-v23.test.ts b/apps/web/src/services/storage/migrations/__tests__/v22-to-v23.test.ts
index 5b764b48..e229ecbc 100644
--- a/apps/web/src/services/storage/migrations/__tests__/v22-to-v23.test.ts
+++ b/apps/web/src/services/storage/migrations/__tests__/v22-to-v23.test.ts
@@ -1,5 +1,6 @@
import { describe, expect, test } from "bun:test";
import { transformProjectV22ToV23 } from "../transformers/v22-to-v23";
+import { asRecord, asRecordArray } from "./helpers";
describe("V22 to V23 Migration", () => {
test("converts project time values from seconds to ticks and fps to a frame-rate object", () => {
@@ -101,20 +102,17 @@ describe("V22 to V23 Migration", () => {
expect(result.skipped).toBe(false);
expect(result.project.version).toBe(23);
- const metadata = result.project.metadata as Record<string, unknown>;
+ const metadata = asRecord(result.project.metadata);
expect(metadata.duration).toBe(1_860_000);
- const settings = result.project.settings as Record<string, unknown>;
+ const settings = asRecord(result.project.settings);
expect(settings.fps).toEqual({ numerator: 30_000, denominator: 1_001 });
- const timelineViewState = result.project.timelineViewState as Record<
- string,
- unknown
- >;
+ const timelineViewState = asRecord(result.project.timelineViewState);
expect(timelineViewState.playheadTime).toBe(150_000);
expect(timelineViewState.scrollLeft).toBe(120);
- const scenes = result.project.scenes as Array<Record<string, unknown>>;
+ const scenes = asRecordArray(result.project.scenes);
const scene = scenes[0];
expect(scene.bookmarks).toEqual([
{
@@ -126,8 +124,8 @@ describe("V22 to V23 Migration", () => {
{ time: 540_000 },
]);
- const tracks = scene.tracks as Array<Record<string, unknown>>;
- const elements = tracks[0].elements as Array<Record<string, unknown>>;
+ const tracks = asRecordArray(scene.tracks);
+ const elements = asRecordArray(tracks[0].elements);
const element = elements[0];
expect(element.startTime).toBe(150_000);
expect(element.duration).toBe(660_000);
@@ -135,8 +133,8 @@ describe("V22 to V23 Migration", () => {
expect(element.trimEnd).toBe(60_000);
expect(element.sourceDuration).toBe(750_000);
- const animations = element.animations as Record<string, unknown>;
- const channels = animations.channels as Record<string, Record<string, unknown>>;
+ const animations = asRecord(element.animations);
+ const channels = asRecord(animations.channels);
expect(channels["opacity:value"]).toEqual({
kind: "scalar",
keys: [
diff --git a/apps/web/src/services/storage/migrations/__tests__/v26-to-v27.test.ts b/apps/web/src/services/storage/migrations/__tests__/v26-to-v27.test.ts
index 730d958d..5e16e886 100644
--- a/apps/web/src/services/storage/migrations/__tests__/v26-to-v27.test.ts
+++ b/apps/web/src/services/storage/migrations/__tests__/v26-to-v27.test.ts
@@ -1,5 +1,6 @@
import { describe, expect, test } from "bun:test";
import { transformProjectV26ToV27 } from "../transformers/v26-to-v27";
+import { asRecord, asRecordArray } from "./helpers";
describe("V26 to V27 Migration", () => {
test("converts custom mask paths from JSON strings to typed point arrays", () => {
@@ -59,13 +60,13 @@ describe("V26 to V27 Migration", () => {
expect(result.skipped).toBe(false);
expect(result.project.version).toBe(27);
- const scenes = result.project.scenes as Array<Record<string, unknown>>;
- const tracks = scenes[0].tracks as Record<string, unknown>;
- const mainTrack = tracks.main as Record<string, unknown>;
- const elements = mainTrack.elements as Array<Record<string, unknown>>;
- const masks = elements[0].masks as Array<Record<string, unknown>>;
- const customParams = masks[0].params as Record<string, unknown>;
- const rectangleParams = masks[1].params as Record<string, unknown>;
+ const scenes = asRecordArray(result.project.scenes);
+ const tracks = asRecord(scenes[0].tracks);
+ const mainTrack = asRecord(tracks.main);
+ const elements = asRecordArray(mainTrack.elements);
+ const masks = asRecordArray(elements[0].masks);
+ const customParams = asRecord(masks[0].params);
+ const rectangleParams = asRecord(masks[1].params);
expect(customParams.path).toEqual([
{
diff --git a/apps/web/src/services/storage/migrations/__tests__/v27-to-v28.test.ts b/apps/web/src/services/storage/migrations/__tests__/v27-to-v28.test.ts
index d467c8a1..cb09e07d 100644
--- a/apps/web/src/services/storage/migrations/__tests__/v27-to-v28.test.ts
+++ b/apps/web/src/services/storage/migrations/__tests__/v27-to-v28.test.ts
@@ -1,5 +1,6 @@
import { describe, expect, test } from "bun:test";
import { transformProjectV27ToV28 } from "../transformers/v27-to-v28";
+import { asRecord, asRecordArray } from "./helpers";
describe("V27 to V28 Migration", () => {
test("rounds persisted media-time floats back to integer ticks", () => {
@@ -96,18 +97,15 @@ describe("V27 to V28 Migration", () => {
expect(result.skipped).toBe(false);
expect(result.project.version).toBe(28);
- const metadata = result.project.metadata as Record<string, unknown>;
+ const metadata = asRecord(result.project.metadata);
expect(metadata.duration).toBe(2_152_466);
- const timelineViewState = result.project.timelineViewState as Record<
- string,
- unknown
- >;
+ const timelineViewState = asRecord(result.project.timelineViewState);
expect(timelineViewState.playheadTime).toBe(301_235);
expect(timelineViewState.zoomLevel).toBe(1.25);
expect(timelineViewState.scrollLeft).toBe(120);
- const scenes = result.project.scenes as Array<Record<string, unknown>>;
+ const scenes = asRecordArray(result.project.scenes);
const scene = scenes[0];
expect(scene.bookmarks).toEqual([
{
@@ -118,9 +116,9 @@ describe("V27 to V28 Migration", () => {
},
]);
- const tracks = scene.tracks as Record<string, unknown>;
- const mainTrack = tracks.main as Record<string, unknown>;
- const elements = mainTrack.elements as Array<Record<string, unknown>>;
+ const tracks = asRecord(scene.tracks);
+ const mainTrack = asRecord(tracks.main);
+ const elements = asRecordArray(mainTrack.elements);
const element = elements[0];
expect(element.startTime).toBe(300_000);
expect(element.duration).toBe(2_152_466);
@@ -128,9 +126,9 @@ describe("V27 to V28 Migration", () => {
expect(element.trimEnd).toBe(15_001);
expect(element.sourceDuration).toBe(2_197_468);
- const animations = element.animations as Record<string, unknown>;
- const channels = animations.channels as Record<string, Record<string, unknown>>;
- const opacityChannel = channels.opacity;
+ const animations = asRecord(element.animations);
+ const channels = asRecord(animations.channels);
+ const opacityChannel = asRecord(channels.opacity);
expect(opacityChannel.keys).toEqual([
{
id: "key-1",
@@ -204,26 +202,23 @@ describe("V27 to V28 Migration", () => {
expect(result.skipped).toBe(false);
expect(result.project.version).toBe(28);
- const metadata = result.project.metadata as Record<string, unknown>;
+ const metadata = asRecord(result.project.metadata);
expect(metadata.duration).toBe(120_000);
- const timelineViewState = result.project.timelineViewState as Record<
- string,
- unknown
- >;
+ const timelineViewState = asRecord(result.project.timelineViewState);
expect(timelineViewState).toEqual({
zoomLevel: 2,
scrollLeft: 300,
playheadTime: 30_000,
});
- const scenes = result.project.scenes as Array<Record<string, unknown>>;
+ const scenes = asRecordArray(result.project.scenes);
const scene = scenes[0];
expect(scene.bookmarks).toEqual([{ time: 60_000, duration: 15_000 }]);
- const tracks = scene.tracks as Record<string, unknown>;
- const mainTrack = tracks.main as Record<string, unknown>;
- const element = (mainTrack.elements as Array<Record<string, unknown>>)[0];
+ const tracks = asRecord(scene.tracks);
+ const mainTrack = asRecord(tracks.main);
+ const element = asRecordArray(mainTrack.elements)[0];
expect(element.startTime).toBe(10_000);
expect(element.duration).toBe(20_000);
expect(element.trimStart).toBe(1_000);
diff --git a/apps/web/src/services/storage/migrations/__tests__/v3-to-v4.test.ts b/apps/web/src/services/storage/migrations/__tests__/v3-to-v4.test.ts
index 6c2d6597..e5ad7e34 100644
--- a/apps/web/src/services/storage/migrations/__tests__/v3-to-v4.test.ts
+++ b/apps/web/src/services/storage/migrations/__tests__/v3-to-v4.test.ts
@@ -1,6 +1,7 @@
import { describe, expect, test } from "bun:test";
import { transformProjectV3ToV4 } from "../transformers/v3-to-v4";
import { v3Project } from "./fixtures";
+import { asRecordArray } from "./helpers";
describe("V3 to V4 Migration", () => {
test("normalizes legacy text fontWeight values", () => {
@@ -54,15 +55,9 @@ describe("V3 to V4 Migration", () => {
expect(result.skipped).toBe(false);
expect(result.project.version).toBe(4);
- const migratedScene = (
- result.project.scenes as Array<Record<string, unknown>>
- )[0];
- const migratedTrack = (
- migratedScene.tracks as Array<Record<string, unknown>>
- )[0];
- const migratedElement = (
- migratedTrack.elements as Array<Record<string, unknown>>
- )[0];
+ const migratedScene = asRecordArray(result.project.scenes)[0];
+ const migratedTrack = asRecordArray(migratedScene.tracks)[0];
+ const migratedElement = asRecordArray(migratedTrack.elements)[0];
expect(migratedElement.fontWeight).toBe("700");
});
@@ -104,15 +99,9 @@ describe("V3 to V4 Migration", () => {
};
const result = transformProjectV3ToV4({ project: projectWithoutTextTrack });
- const migratedScene = (
- result.project.scenes as Array<Record<string, unknown>>
- )[0];
- const migratedTrack = (
- migratedScene.tracks as Array<Record<string, unknown>>
- )[0];
- const migratedElement = (
- migratedTrack.elements as Array<Record<string, unknown>>
- )[0];
+ const migratedScene = asRecordArray(result.project.scenes)[0];
+ const migratedTrack = asRecordArray(migratedScene.tracks)[0];
+ const migratedElement = asRecordArray(migratedTrack.elements)[0];
expect(migratedElement.iconName).toBe("mdi:home");
});
diff --git a/apps/web/src/services/storage/migrations/__tests__/v4-to-v5.test.ts b/apps/web/src/services/storage/migrations/__tests__/v4-to-v5.test.ts
index 95386137..16e9e7ca 100644
--- a/apps/web/src/services/storage/migrations/__tests__/v4-to-v5.test.ts
+++ b/apps/web/src/services/storage/migrations/__tests__/v4-to-v5.test.ts
@@ -1,6 +1,7 @@
import { describe, expect, test } from "bun:test";
import { transformProjectV4ToV5 } from "../transformers/v4-to-v5";
import { v3Project } from "./fixtures";
+import { asRecordArray } from "./helpers";
describe("V4 to V5 Migration", () => {
test("migrates sticker iconName to stickerId and removes legacy color", () => {
@@ -48,15 +49,9 @@ describe("V4 to V5 Migration", () => {
expect(result.skipped).toBe(false);
expect(result.project.version).toBe(5);
- const migratedScene = (
- result.project.scenes as Array<Record<string, unknown>>
- )[0];
- const migratedTrack = (
- migratedScene.tracks as Array<Record<string, unknown>>
- )[0];
- const migratedElement = (
- migratedTrack.elements as Array<Record<string, unknown>>
- )[0];
+ const migratedScene = asRecordArray(result.project.scenes)[0];
+ const migratedTrack = asRecordArray(migratedScene.tracks)[0];
+ const migratedElement = asRecordArray(migratedTrack.elements)[0];
expect(migratedElement.stickerId).toBe("icons:mdi:home");
expect("iconName" in migratedElement).toBe(false);
@@ -101,15 +96,9 @@ describe("V4 to V5 Migration", () => {
};
const result = transformProjectV4ToV5({ project: projectWithStickerId });
- const migratedScene = (
- result.project.scenes as Array<Record<string, unknown>>
- )[0];
- const migratedTrack = (
- migratedScene.tracks as Array<Record<string, unknown>>
- )[0];
- const migratedElement = (
- migratedTrack.elements as Array<Record<string, unknown>>
- )[0];
+ const migratedScene = asRecordArray(result.project.scenes)[0];
+ const migratedTrack = asRecordArray(migratedScene.tracks)[0];
+ const migratedElement = asRecordArray(migratedTrack.elements)[0];
expect(migratedElement.stickerId).toBe("flags:AD");
});
diff --git a/apps/web/src/services/storage/migrations/__tests__/v5-to-v6.test.ts b/apps/web/src/services/storage/migrations/__tests__/v5-to-v6.test.ts
index b05d1837..8fd267fd 100644
--- a/apps/web/src/services/storage/migrations/__tests__/v5-to-v6.test.ts
+++ b/apps/web/src/services/storage/migrations/__tests__/v5-to-v6.test.ts
@@ -1,6 +1,7 @@
import { describe, expect, test } from "bun:test";
import { transformProjectV5ToV6 } from "../transformers/v5-to-v6";
import { v5Project } from "./fixtures";
+import { asRecordArray } from "./helpers";
describe("V5 to V6 Migration", () => {
test("converts number bookmarks to Bookmark objects", async () => {
@@ -13,19 +14,13 @@ describe("V5 to V6 Migration", () => {
expect(result.skipped).toBe(false);
expect(result.project.version).toBe(6);
- const mainScene = (
- result.project.scenes as Array<{ bookmarks: unknown[] }>
- )[0];
- expect(mainScene.bookmarks).toEqual([
+ const scenes = asRecordArray(result.project.scenes);
+ expect(scenes[0].bookmarks).toEqual([
{ time: 2.0 },
{ time: 5.5 },
{ time: 12.0 },
]);
-
- const introScene = (
- result.project.scenes as Array<{ bookmarks: unknown[] }>
- )[1];
- expect(introScene.bookmarks).toEqual([]);
+ expect(scenes[1].bookmarks).toEqual([]);
});
test("skips projects that are already v6", () => {
@@ -83,9 +78,7 @@ describe("V5 to V6 Migration", () => {
});
expect(result.skipped).toBe(false);
- const mainScene = (
- result.project.scenes as Array<{ bookmarks: unknown[] }>
- )[0];
+ const mainScene = asRecordArray(result.project.scenes)[0];
expect(mainScene.bookmarks).toEqual([
{ time: 1, note: "Intro", color: "#ef4444" },
{ time: 5.5, duration: 2 },
diff --git a/apps/web/src/services/storage/migrations/__tests__/v8-to-v9.test.ts b/apps/web/src/services/storage/migrations/__tests__/v8-to-v9.test.ts
index aa79a6ea..ce8e9bd3 100644
--- a/apps/web/src/services/storage/migrations/__tests__/v8-to-v9.test.ts
+++ b/apps/web/src/services/storage/migrations/__tests__/v8-to-v9.test.ts
@@ -1,5 +1,6 @@
import { describe, expect, test } from "bun:test";
import { transformProjectV8ToV9 } from "../transformers/v8-to-v9";
+import { asRecord, asRecordArray } from "./helpers";
const v8ProjectWithText = {
id: "project-v8-text",
@@ -70,16 +71,16 @@ describe("V8 to V9 Migration", () => {
expect(result.skipped).toBe(false);
expect(result.project.version).toBe(9);
- const track = (
- result.project.scenes as Array<{ tracks: Array<{ elements: unknown[] }> }>
- )[0].tracks[0];
- const elements = track.elements as Array<{ background: { enabled: boolean; color: string } }>;
+ const track = asRecordArray(asRecordArray(result.project.scenes)[0].tracks)[0];
+ const elements = asRecordArray(track.elements);
+ const background0 = asRecord(elements[0].background);
+ const background1 = asRecord(elements[1].background);
- expect(elements[0].background.enabled).toBe(true);
- expect(elements[0].background.color).toBe("#ff0000");
+ expect(background0.enabled).toBe(true);
+ expect(background0.color).toBe("#ff0000");
- expect(elements[1].background.enabled).toBe(false);
- expect(elements[1].background.color).toBe("transparent");
+ expect(background1.enabled).toBe(false);
+ expect(background1.color).toBe("transparent");
});
test("preserves existing background.enabled if already present", () => {
@@ -87,7 +88,7 @@ describe("V8 to V9 Migration", () => {
...v8ProjectWithText,
scenes: [
{
- ...(v8ProjectWithText.scenes as Record<string, unknown>[])[0],
+ ...asRecordArray(v8ProjectWithText.scenes)[0],
tracks: [
{
id: "track-text",
@@ -116,10 +117,9 @@ describe("V8 to V9 Migration", () => {
const result = transformProjectV8ToV9({ project: projectWithEnabled });
expect(result.skipped).toBe(false);
- const elements = (
- result.project.scenes as Array<{ tracks: Array<{ elements: unknown[] }> }>
- )[0].tracks[0].elements as Array<{ background: { enabled: boolean } }>;
- expect(elements[0].background.enabled).toBe(false);
+ const track = asRecordArray(asRecordArray(result.project.scenes)[0].tracks)[0];
+ const elements = asRecordArray(track.elements);
+ expect(asRecord(elements[0].background).enabled).toBe(false);
});
test("skips non-text elements and tracks", () => {
diff --git a/apps/web/src/services/storage/migrations/transformers/v1-to-v2.ts b/apps/web/src/services/storage/migrations/transformers/v1-to-v2.ts
index 065a6b34..e9e23c4e 100644
--- a/apps/web/src/services/storage/migrations/transformers/v1-to-v2.ts
+++ b/apps/web/src/services/storage/migrations/transformers/v1-to-v2.ts
@@ -19,34 +19,6 @@ const EMPTY_V1_TO_V2_CONTEXT: V1ToV2Context = {
mediaTypesById: {},
};
-interface LegacyMediaElement {
- type: "media";
- mediaId: string;
- muted?: boolean;
- [key: string]: unknown;
-}
-
-interface LegacyTextElement {
- type: "text";
- x: number;
- y: number;
- rotation: number;
- opacity: number;
- [key: string]: unknown;
-}
-
-interface LegacyAudioElement {
- type: "audio";
- mediaId: string;
- [key: string]: unknown;
-}
-
-interface LegacyMediaTrack {
- type: "media";
- elements: unknown[];
- [key: string]: unknown;
-}
-
interface V2Transform {
scale: number;
position: { x: number; y: number };
@@ -315,7 +287,7 @@ function transformTracks({
const isMain = !isFirstVideoTrackFound;
isFirstVideoTrackFound = true;
const videoTrack = transformMediaTrack({
- track: track as LegacyMediaTrack,
+ track,
context,
isMain,
});
@@ -343,7 +315,7 @@ function transformMediaTrack({
context,
isMain,
}: {
- track: LegacyMediaTrack;
+ track: Record<string, unknown>;
context: V1ToV2Context;
isMain: boolean;
}): V2VideoTrack {
@@ -354,8 +326,7 @@ function transformMediaTrack({
return null;
}
- const mediaElement = element as LegacyMediaElement;
- const mediaId = getStringValue({ value: mediaElement.mediaId });
+ const mediaId = getStringValue({ value: element.mediaId });
if (!mediaId) {
return null;
}
@@ -372,7 +343,7 @@ function transformMediaTrack({
rotate: 0,
};
- const muted = mediaElement.muted === true;
+ const muted = element.muted === true;
if (mediaType === "image") {
const imageElement: V2ImageElement = {
@@ -442,15 +413,14 @@ function transformTextTrack({
return null;
}
- const textElement = element as LegacyTextElement;
- const x = getNumberValue({ value: textElement.x, fallback: 0 });
- const y = getNumberValue({ value: textElement.y, fallback: 0 });
+ const x = getNumberValue({ value: element.x, fallback: 0 });
+ const y = getNumberValue({ value: element.y, fallback: 0 });
const rotation = getNumberValue({
- value: textElement.rotation,
+ value: element.rotation,
fallback: 0,
});
const opacity = getNumberValue({
- value: textElement.opacity,
+ value: element.opacity,
fallback: 1,
});
@@ -464,23 +434,23 @@ function transformTextTrack({
id: getStringValue({ value: element.id, fallback: "" }),
name: getStringValue({ value: element.name, fallback: "" }),
type: "text",
- content: getStringValue({ value: textElement.content, fallback: "" }),
+ content: getStringValue({ value: element.content, fallback: "" }),
fontSize: getNumberValue({
- value: textElement.fontSize,
+ value: element.fontSize,
fallback: 16,
}),
fontFamily: getStringValue({
- value: textElement.fontFamily,
+ value: element.fontFamily,
fallback: "Arial",
}),
color: getStringValue({
- value: textElement.color,
+ value: element.color,
fallback: "#000000",
}),
background: {
enabled: false,
color: getStringValue({
- value: textElement.backgroundColor,
+ value: element.backgroundColor,
fallback: "transparent",
}),
cornerRadius: 0,
@@ -489,22 +459,26 @@ function transformTextTrack({
offsetX: 0,
offsetY: 0,
},
- textAlign: (getStringValue({
- value: textElement.textAlign,
+ textAlign: parseEnum({
+ value: element.textAlign,
+ allowed: ["left", "center", "right"] as const,
fallback: "left",
- }) || "left") as "left" | "center" | "right",
- fontWeight: (getStringValue({
- value: textElement.fontWeight,
+ }),
+ fontWeight: parseEnum({
+ value: element.fontWeight,
+ allowed: ["normal", "bold"] as const,
fallback: "normal",
- }) || "normal") as "normal" | "bold",
- fontStyle: (getStringValue({
- value: textElement.fontStyle,
+ }),
+ fontStyle: parseEnum({
+ value: element.fontStyle,
+ allowed: ["normal", "italic"] as const,
fallback: "normal",
- }) || "normal") as "normal" | "italic",
- textDecoration: (getStringValue({
- value: textElement.textDecoration,
+ }),
+ textDecoration: parseEnum({
+ value: element.textDecoration,
+ allowed: ["none", "underline", "line-through"] as const,
fallback: "none",
- }) || "none") as "none" | "underline" | "line-through",
+ }),
hidden: false,
transform,
opacity,
@@ -538,8 +512,7 @@ function transformAudioTrack({
return null;
}
- const audioElement = element as LegacyAudioElement;
- const mediaId = getStringValue({ value: audioElement.mediaId });
+ const mediaId = getStringValue({ value: element.mediaId });
if (!mediaId) {
return null;
}
@@ -762,6 +735,23 @@ function getStringValue({
return fallback;
}
+function parseEnum<T extends string>({
+ value,
+ allowed,
+ fallback,
+}: {
+ value: unknown;
+ allowed: readonly T[];
+ fallback: T;
+}): T {
+ for (const candidate of allowed) {
+ if (value === candidate) {
+ return candidate;
+ }
+ }
+ return fallback;
+}
+
function normalizeDateString({ value }: { value: unknown }): string {
if (value instanceof Date) {
return value.toISOString();
diff --git a/apps/web/src/services/storage/migrations/transformers/v13-to-v14.ts b/apps/web/src/services/storage/migrations/transformers/v13-to-v14.ts
index 89dde38e..6e152adb 100644
--- a/apps/web/src/services/storage/migrations/transformers/v13-to-v14.ts
+++ b/apps/web/src/services/storage/migrations/transformers/v13-to-v14.ts
@@ -94,10 +94,7 @@ function migrateSplitMask({ mask }: { mask: unknown }): unknown {
const x = (position - 0.5) * Math.cos(angleRad);
const y = (position - 0.5) * Math.sin(angleRad);
- const { position: _removed, ...restParams } = params as Record<
- string,
- unknown
- > & { position: unknown };
+ const { position: _removed, ...restParams } = params;
return {
...mask,
diff --git a/apps/web/src/services/storage/migrations/transformers/v26-to-v27.ts b/apps/web/src/services/storage/migrations/transformers/v26-to-v27.ts
index 3b118ee7..4477d363 100644
--- a/apps/web/src/services/storage/migrations/transformers/v26-to-v27.ts
+++ b/apps/web/src/services/storage/migrations/transformers/v26-to-v27.ts
@@ -12,19 +12,18 @@ interface CustomMaskPathPoint {
}
function isCustomMaskPathPoint(value: unknown): value is CustomMaskPathPoint {
- if (!value || typeof value !== "object") {
+ if (!isRecord(value)) {
return false;
}
- const candidate = value as Record<string, unknown>;
return (
- typeof candidate.id === "string" &&
- typeof candidate.x === "number" &&
- typeof candidate.y === "number" &&
- typeof candidate.inX === "number" &&
- typeof candidate.inY === "number" &&
- typeof candidate.outX === "number" &&
- typeof candidate.outY === "number"
+ typeof value.id === "string" &&
+ typeof value.x === "number" &&
+ typeof value.y === "number" &&
+ typeof value.inX === "number" &&
+ typeof value.inY === "number" &&
+ typeof value.outX === "number" &&
+ typeof value.outY === "number"
);
}
diff --git a/apps/web/src/services/storage/migrations/transformers/v3-to-v4.ts b/apps/web/src/services/storage/migrations/transformers/v3-to-v4.ts
index 84d97fd8..7a28134b 100644
--- a/apps/web/src/services/storage/migrations/transformers/v3-to-v4.ts
+++ b/apps/web/src/services/storage/migrations/transformers/v3-to-v4.ts
@@ -1,10 +1,10 @@
import type { MigrationResult, ProjectRecord } from "./types";
import { getProjectId, isRecord } from "./utils";
-const LEGACY_FONT_WEIGHT_MAP = {
- normal: "400",
- bold: "700",
-} as const;
+const LEGACY_FONT_WEIGHT_MAP = new Map<string, string>([
+ ["normal", "400"],
+ ["bold", "700"],
+]);
const VALID_NUMERIC_FONT_WEIGHTS = new Set([
"100",
@@ -168,10 +168,9 @@ function normalizeFontWeight({ value }: { value: unknown }): unknown {
}
const normalized = value.trim().toLowerCase();
- if (normalized in LEGACY_FONT_WEIGHT_MAP) {
- return LEGACY_FONT_WEIGHT_MAP[
- normalized as keyof typeof LEGACY_FONT_WEIGHT_MAP
- ];
+ const mapped = LEGACY_FONT_WEIGHT_MAP.get(normalized);
+ if (mapped !== undefined) {
+ return mapped;
}
if (VALID_NUMERIC_FONT_WEIGHTS.has(normalized)) {