From f5f31b43df9ef46f1afdd63ba759b2f9bd80e7a3 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Wed, 22 Apr 2026 02:41:18 +0200 Subject: [PATCH] refactor: migration run() and pure v1-to-v2 transform --- .../migrations/__tests__/v1-to-v2.test.ts | 172 ++++++---- .../src/services/storage/migrations/base.ts | 25 +- .../src/services/storage/migrations/runner.ts | 315 +++++++++--------- .../migrations/transformers/v1-to-v2.ts | 315 +++++++----------- .../migrations/transformers/v14-to-v15.ts | 136 ++++---- .../migrations/transformers/v17-to-v18.ts | 218 ++++++------ .../migrations/transformers/v20-to-v21.ts | 3 +- .../migrations/transformers/v22-to-v23.ts | 10 +- .../migrations/transformers/v26-to-v27.ts | 45 ++- .../services/storage/migrations/v0-to-v1.ts | 12 +- .../services/storage/migrations/v1-to-v2.ts | 287 ++++++++++++---- .../services/storage/migrations/v10-to-v11.ts | 12 +- .../services/storage/migrations/v11-to-v12.ts | 12 +- .../services/storage/migrations/v12-to-v13.ts | 12 +- .../services/storage/migrations/v13-to-v14.ts | 12 +- .../services/storage/migrations/v14-to-v15.ts | 12 +- .../services/storage/migrations/v15-to-v16.ts | 12 +- .../services/storage/migrations/v16-to-v17.ts | 12 +- .../services/storage/migrations/v17-to-v18.ts | 12 +- .../services/storage/migrations/v18-to-v19.ts | 12 +- .../services/storage/migrations/v19-to-v20.ts | 12 +- .../services/storage/migrations/v2-to-v3.ts | 12 +- .../services/storage/migrations/v20-to-v21.ts | 12 +- .../services/storage/migrations/v21-to-v22.ts | 12 +- .../services/storage/migrations/v22-to-v23.ts | 12 +- .../services/storage/migrations/v23-to-v24.ts | 12 +- .../services/storage/migrations/v24-to-v25.ts | 12 +- .../services/storage/migrations/v25-to-v26.ts | 12 +- .../services/storage/migrations/v26-to-v27.ts | 12 +- .../services/storage/migrations/v3-to-v4.ts | 12 +- .../services/storage/migrations/v4-to-v5.ts | 12 +- .../services/storage/migrations/v5-to-v6.ts | 12 +- .../services/storage/migrations/v6-to-v7.ts | 12 +- .../services/storage/migrations/v7-to-v8.ts | 12 +- .../services/storage/migrations/v8-to-v9.ts | 12 +- .../services/storage/migrations/v9-to-v10.ts | 12 +- 36 files changed, 1000 insertions(+), 838 deletions(-) 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 6d0c37fb..0840ac76 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 @@ -1,10 +1,9 @@ import { describe, expect, test } from "bun:test"; -import { DEFAULT_BACKGROUND_BLUR_INTENSITY } from "@/background/blur"; -import { DEFAULT_BACKGROUND_COLOR } from "@/background/color"; -import { DEFAULT_CANVAS_SIZE } from "@/canvas/sizes"; -const DEFAULT_FPS = 30; -import type { MediaAssetData } from "@/services/storage/types"; -import { getProjectId, transformProjectV1ToV2 } from "../transformers/v1-to-v2"; +import { + getProjectId, + transformProjectV1ToV2, + type V1ToV2Context, +} from "../transformers/v1-to-v2"; import { projectWithNoId, projectWithNullValues, @@ -13,10 +12,15 @@ import { v2Project, } from "./fixtures"; +const DEFAULT_FPS = 30; +const DEFAULT_BACKGROUND_BLUR_INTENSITY = 10; +const DEFAULT_BACKGROUND_COLOR = "#000000"; +const DEFAULT_CANVAS_SIZE = { width: 1920, height: 1080 }; + describe("V1 to V2 Migration", () => { describe("transformProjectV1ToV2", () => { - test("creates metadata object from flat properties", async () => { - const result = await transformProjectV1ToV2({ project: v1Project }); + test("creates metadata object from flat properties", () => { + const result = transformProjectV1ToV2({ project: v1Project }); expect(result.skipped).toBe(false); expect(result.project.version).toBe(2); @@ -28,8 +32,8 @@ describe("V1 to V2 Migration", () => { expect(typeof metadata.updatedAt).toBe("string"); }); - test("creates settings object from flat properties", async () => { - const result = await transformProjectV1ToV2({ project: v1Project }); + test("creates settings object from flat properties", () => { + const result = transformProjectV1ToV2({ project: v1Project }); const settings = result.project.settings as Record; expect(settings.fps).toBe(v1Project.fps); @@ -37,8 +41,8 @@ describe("V1 to V2 Migration", () => { expect(settings.originalCanvasSize).toBe(null); }); - test("converts color background correctly", async () => { - const result = await transformProjectV1ToV2({ project: v1Project }); + test("converts color background correctly", () => { + const result = transformProjectV1ToV2({ project: v1Project }); const settings = result.project.settings as Record; const background = settings.background as Record; @@ -46,13 +50,13 @@ describe("V1 to V2 Migration", () => { expect(background.color).toBe(v1Project.backgroundColor); }); - test("converts blur background correctly", async () => { + test("converts blur background correctly", () => { const projectWithBlur = { ...v1Project, backgroundType: "blur", blurIntensity: 30, }; - const result = await transformProjectV1ToV2({ project: projectWithBlur }); + const result = transformProjectV1ToV2({ project: projectWithBlur }); const settings = result.project.settings as Record; const background = settings.background as Record; @@ -60,16 +64,16 @@ describe("V1 to V2 Migration", () => { expect(background.blurIntensity).toBe(30); }); - test("applies legacy bookmarks to main scene", async () => { - const result = await transformProjectV1ToV2({ project: v1Project }); + test("applies legacy bookmarks to main scene", () => { + const result = transformProjectV1ToV2({ project: v1Project }); const scenes = result.project.scenes as Array>; const mainScene = scenes.find((s) => s.isMain === true); expect(mainScene?.bookmarks).toEqual(v1Project.bookmarks); }); - test("preserves existing scene bookmarks", async () => { - const result = await transformProjectV1ToV2({ + test("preserves existing scene bookmarks", () => { + const result = transformProjectV1ToV2({ project: v1ProjectWithMultipleScenes, }); @@ -78,22 +82,22 @@ describe("V1 to V2 Migration", () => { expect(introScene?.bookmarks).toEqual([1.0]); }); - test("skips project that already has v2 structure", async () => { - const result = await transformProjectV1ToV2({ project: v2Project }); + test("skips project that already has v2 structure", () => { + const result = transformProjectV1ToV2({ project: v2Project }); expect(result.skipped).toBe(true); expect(result.reason).toBe("already v2"); }); - test("skips project with no id", async () => { - const result = await transformProjectV1ToV2({ project: projectWithNoId }); + test("skips project with no id", () => { + const result = transformProjectV1ToV2({ project: projectWithNoId }); expect(result.skipped).toBe(true); expect(result.reason).toBe("no project id"); }); - test("handles null values gracefully", async () => { - const result = await transformProjectV1ToV2({ + test("handles null values gracefully", () => { + const result = transformProjectV1ToV2({ project: projectWithNullValues, }); @@ -103,13 +107,13 @@ describe("V1 to V2 Migration", () => { expect(settings.canvasSize).toEqual(DEFAULT_CANVAS_SIZE); }); - test("uses default values for missing properties", async () => { + test("uses default values for missing properties", () => { const minimalProject = { id: "minimal", version: 1, scenes: [], }; - const result = await transformProjectV1ToV2({ project: minimalProject }); + const result = transformProjectV1ToV2({ project: minimalProject }); const settings = result.project.settings as Record; expect(settings.fps).toBe(DEFAULT_FPS); @@ -120,14 +124,14 @@ describe("V1 to V2 Migration", () => { expect(background.color).toBe(DEFAULT_BACKGROUND_COLOR); }); - test("uses default blur intensity when missing", async () => { + test("uses default blur intensity when missing", () => { const projectWithBlurNoIntensity = { id: "blur-no-intensity", version: 1, backgroundType: "blur", scenes: [], }; - const result = await transformProjectV1ToV2({ + const result = transformProjectV1ToV2({ project: projectWithBlurNoIntensity, }); @@ -136,23 +140,23 @@ describe("V1 to V2 Migration", () => { expect(background.blurIntensity).toBe(DEFAULT_BACKGROUND_BLUR_INTENSITY); }); - test("preserves currentSceneId", async () => { - const result = await transformProjectV1ToV2({ project: v1Project }); + test("preserves currentSceneId", () => { + const result = transformProjectV1ToV2({ project: v1Project }); expect(result.project.currentSceneId).toBe(v1Project.currentSceneId); }); - test("finds main scene id when currentSceneId missing", async () => { + test("finds main scene id when currentSceneId missing", () => { const projectWithoutCurrentScene = { ...v1Project, currentSceneId: undefined, }; - const result = await transformProjectV1ToV2({ + const result = transformProjectV1ToV2({ project: projectWithoutCurrentScene, }); expect(result.project.currentSceneId).toBe("scene-main"); }); - test("skips loading tracks if scene already has tracks", async () => { + test("skips loading tracks if scene already has tracks", () => { const projectWithTracks = { ...v1Project, scenes: [ @@ -175,7 +179,7 @@ describe("V1 to V2 Migration", () => { ], }; - const result = await transformProjectV1ToV2({ + const result = transformProjectV1ToV2({ project: projectWithTracks, }); @@ -188,22 +192,32 @@ describe("V1 to V2 Migration", () => { }); describe("Track Loading and Transformation", () => { - test("loads tracks from legacy DB and transforms media track to video track", async () => { - const mockLoadMediaAsset = async ({ - mediaId, - }: { - mediaId: string; - }): Promise => { - if (mediaId === "media-1") { - return { - id: "media-1", - name: "Test Video", - type: "video", - size: 1000, - lastModified: Date.now(), - }; - } - return null; + test("loads tracks from legacy DB and transforms media track to video track", () => { + const context: V1ToV2Context = { + legacyTracksBySceneId: { + "scene-main": [ + { + id: "legacy-track-1", + type: "media", + name: "Legacy media track", + elements: [ + { + id: "media-element-1", + name: "Test video clip", + type: "media", + mediaId: "media-1", + duration: 120, + startTime: 0, + trimStart: 0, + trimEnd: 0, + }, + ], + }, + ], + }, + mediaTypesById: { + "media-1": "video", + }, }; const projectWithLegacyTracks = { @@ -221,19 +235,50 @@ describe("V1 to V2 Migration", () => { ], }; - // mock IndexedDB for this test would require setting up a test environment - // for now, we test that the transformer handles empty tracks gracefully - const result = await transformProjectV1ToV2({ + const result = transformProjectV1ToV2({ project: projectWithLegacyTracks, - options: { loadMediaAsset: mockLoadMediaAsset }, + context, }); const scenes = result.project.scenes as Array>; const mainScene = scenes[0]; - expect(Array.isArray(mainScene.tracks)).toBe(true); + const tracks = mainScene.tracks as Array>; + expect(Array.isArray(tracks)).toBe(true); + expect(tracks).toHaveLength(1); + expect(tracks[0].type).toBe("video"); + expect(tracks[0].isMain).toBe(true); }); - test("transforms text element preserving opacity and migrating position", async () => { + test("transforms text element preserving opacity and migrating position", () => { + const context: V1ToV2Context = { + legacyTracksBySceneId: { + "scene-1": [ + { + id: "legacy-text-track", + type: "text", + name: "Text", + elements: [ + { + id: "text-element-1", + name: "Title", + type: "text", + content: "Hello", + x: 120, + y: 240, + rotation: 15, + opacity: 0.5, + duration: 90, + startTime: 10, + trimStart: 0, + trimEnd: 0, + }, + ], + }, + ], + }, + mediaTypesById: {}, + }; + const projectWithTextTrack = { id: "project-text", version: 1, @@ -251,15 +296,22 @@ describe("V1 to V2 Migration", () => { ], }; - // since tracks are empty, transformation won't happen - // but we verify the structure is correct - const result = await transformProjectV1ToV2({ + const result = transformProjectV1ToV2({ project: projectWithTextTrack, + context, }); expect(result.skipped).toBe(false); const scenes = result.project.scenes as Array>; - expect(scenes.length).toBe(1); + const tracks = scenes[0].tracks as Array>; + const elements = tracks[0].elements as Array>; + const textElement = elements[0]; + expect(textElement.opacity).toBe(0.5); + expect(textElement.transform).toEqual({ + scale: 1, + position: { x: 120, y: 240 }, + rotate: 15, + }); }); }); diff --git a/apps/web/src/services/storage/migrations/base.ts b/apps/web/src/services/storage/migrations/base.ts index 13418f98..0130c95e 100644 --- a/apps/web/src/services/storage/migrations/base.ts +++ b/apps/web/src/services/storage/migrations/base.ts @@ -1,9 +1,16 @@ -import type { MigrationResult, ProjectRecord } from "./transformers/types"; - -export abstract class StorageMigration { - abstract from: number; - abstract to: number; - abstract transform( - project: ProjectRecord, - ): Promise>; -} +import type { MigrationResult, ProjectRecord } from "./transformers/types"; + +export interface StorageMigrationRunArgs { + projectId: string; + project: ProjectRecord; +} + +export abstract class StorageMigration { + abstract from: number; + abstract to: number; + + abstract run({ + projectId, + project, + }: StorageMigrationRunArgs): Promise>; +} diff --git a/apps/web/src/services/storage/migrations/runner.ts b/apps/web/src/services/storage/migrations/runner.ts index f63fe983..eed8071f 100644 --- a/apps/web/src/services/storage/migrations/runner.ts +++ b/apps/web/src/services/storage/migrations/runner.ts @@ -1,156 +1,159 @@ -import { - IndexedDBAdapter, - deleteDatabase, -} from "@/services/storage/indexeddb-adapter"; -import type { StorageMigration } from "./base"; -import type { ProjectRecord } from "./transformers/types"; -import { getProjectId, isRecord } from "./transformers/utils"; - -export interface StorageMigrationResult { - migratedCount: number; -} - -export interface MigrationProgress { - isMigrating: boolean; - fromVersion: number | null; - toVersion: number | null; - projectName: string | null; -} - -let hasCleanedUpMetaDb = false; - -const MIN_MIGRATION_DISPLAY_MS = 1000; - -export async function runStorageMigrations({ - migrations, - onProgress, -}: { - migrations: StorageMigration[]; - onProgress?: (progress: MigrationProgress) => void; -}): Promise { - // One-time cleanup: delete the old global version database - if (!hasCleanedUpMetaDb) { - try { - await deleteDatabase({ dbName: "video-editor-meta" }); - } catch { - // Ignore errors - DB might not exist - } - hasCleanedUpMetaDb = true; - } - - const projectsAdapter = new IndexedDBAdapter( - "video-editor-projects", - "projects", - 1, - ); - - const projects = await projectsAdapter.getAll(); - - const orderedMigrations = [...migrations].sort((a, b) => a.from - b.from); - let migratedCount = 0; - let migrationStartTime: number | null = null; - - for (const project of projects) { - if (typeof project !== "object" || project === null) { - continue; - } - - let projectRecord = project as ProjectRecord; - let currentVersion = getProjectVersion({ project: projectRecord }); - const targetVersion = orderedMigrations.at(-1)?.to ?? currentVersion; - - if (currentVersion >= targetVersion) { - continue; - } - - // Track when we first showed the migration dialog - if (migrationStartTime === null) { - migrationStartTime = Date.now(); - } - - const projectName = getProjectName({ project: projectRecord }); - onProgress?.({ - isMigrating: true, - fromVersion: currentVersion, - toVersion: targetVersion, - projectName, - }); - - for (const migration of orderedMigrations) { - if (migration.from !== currentVersion) { - continue; - } - - const result = await migration.transform(projectRecord); - - if (result.skipped) { - break; - } - - const projectId = getProjectId({ project: result.project }); - if (!projectId) { - break; - } - - await projectsAdapter.set(projectId, result.project); - migratedCount++; - currentVersion = migration.to; - projectRecord = result.project; - } - } - - // Ensure dialog is visible for minimum time so users can see it - if (migrationStartTime !== null) { - const elapsed = Date.now() - migrationStartTime; - if (elapsed < MIN_MIGRATION_DISPLAY_MS) { - await new Promise((resolve) => - setTimeout(resolve, MIN_MIGRATION_DISPLAY_MS - elapsed), - ); - } - } - - onProgress?.({ - isMigrating: false, - fromVersion: null, - toVersion: null, - projectName: null, - }); - - return { migratedCount }; -} - -function getProjectVersion({ project }: { project: ProjectRecord }): number { - const versionValue = project.version; - - // v2 and up - has explicit version field - if (typeof versionValue === "number") { - return versionValue; - } - - // v1 - has scenes array - const scenesValue = project.scenes; - if (Array.isArray(scenesValue) && scenesValue.length > 0) { - return 1; - } - - // v0 - no scenes - return 0; -} - -function getProjectName({ - project, -}: { - project: ProjectRecord; -}): string | null { - const metadata = project.metadata; - if (isRecord(metadata) && typeof metadata.name === "string") { - return metadata.name; - } - - // v0 had name directly on project - if (typeof project.name === "string") { - return project.name; - } - - return null; -} +import { + IndexedDBAdapter, + deleteDatabase, +} from "@/services/storage/indexeddb-adapter"; +import type { StorageMigration } from "./base"; +import type { ProjectRecord } from "./transformers/types"; +import { getProjectId, isRecord } from "./transformers/utils"; + +export interface StorageMigrationResult { + migratedCount: number; +} + +export interface MigrationProgress { + isMigrating: boolean; + fromVersion: number | null; + toVersion: number | null; + projectName: string | null; +} + +let hasCleanedUpMetaDb = false; + +const MIN_MIGRATION_DISPLAY_MS = 1000; + +export async function runStorageMigrations({ + migrations, + onProgress, +}: { + migrations: StorageMigration[]; + onProgress?: (progress: MigrationProgress) => void; +}): Promise { + // One-time cleanup: delete the old global version database + if (!hasCleanedUpMetaDb) { + try { + await deleteDatabase({ dbName: "video-editor-meta" }); + } catch { + // Ignore errors - DB might not exist + } + hasCleanedUpMetaDb = true; + } + + const projectsAdapter = new IndexedDBAdapter( + "video-editor-projects", + "projects", + 1, + ); + + const projects = await projectsAdapter.getAll(); + + const orderedMigrations = [...migrations].sort((a, b) => a.from - b.from); + let migratedCount = 0; + let migrationStartTime: number | null = null; + + for (const project of projects) { + if (typeof project !== "object" || project === null) { + continue; + } + + let projectRecord = project as ProjectRecord; + const projectId = getProjectId({ project: projectRecord }); + if (!projectId) { + continue; + } + + let currentVersion = getProjectVersion({ project: projectRecord }); + const targetVersion = orderedMigrations.at(-1)?.to ?? currentVersion; + + if (currentVersion >= targetVersion) { + continue; + } + + // Track when we first showed the migration dialog + if (migrationStartTime === null) { + migrationStartTime = Date.now(); + } + + const projectName = getProjectName({ project: projectRecord }); + onProgress?.({ + isMigrating: true, + fromVersion: currentVersion, + toVersion: targetVersion, + projectName, + }); + + for (const migration of orderedMigrations) { + if (migration.from !== currentVersion) { + continue; + } + + const result = await migration.run({ + projectId, + project: projectRecord, + }); + + if (result.skipped) { + break; + } + + await projectsAdapter.set(projectId, result.project); + migratedCount++; + currentVersion = migration.to; + projectRecord = result.project; + } + } + + // Ensure dialog is visible for minimum time so users can see it + if (migrationStartTime !== null) { + const elapsed = Date.now() - migrationStartTime; + if (elapsed < MIN_MIGRATION_DISPLAY_MS) { + await new Promise((resolve) => + setTimeout(resolve, MIN_MIGRATION_DISPLAY_MS - elapsed), + ); + } + } + + onProgress?.({ + isMigrating: false, + fromVersion: null, + toVersion: null, + projectName: null, + }); + + return { migratedCount }; +} + +function getProjectVersion({ project }: { project: ProjectRecord }): number { + const versionValue = project.version; + + // v2 and up - has explicit version field + if (typeof versionValue === "number") { + return versionValue; + } + + // v1 - has scenes array + const scenesValue = project.scenes; + if (Array.isArray(scenesValue) && scenesValue.length > 0) { + return 1; + } + + // v0 - no scenes + return 0; +} + +function getProjectName({ + project, +}: { + project: ProjectRecord; +}): string | null { + const metadata = project.metadata; + if (isRecord(metadata) && typeof metadata.name === "string") { + return metadata.name; + } + + // v0 had name directly on project + if (typeof project.name === "string") { + return project.name; + } + + return null; +} 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 c4b64e50..065a6b34 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 @@ -1,17 +1,24 @@ -import { DEFAULT_BACKGROUND_BLUR_INTENSITY } from "@/background/blur"; -import { DEFAULT_BACKGROUND_COLOR } from "@/background/color"; -import { DEFAULT_CANVAS_SIZE } from "@/canvas/sizes"; -const DEFAULT_FPS = 30; -import { IndexedDBAdapter } from "@/services/storage/indexeddb-adapter"; -import type { MediaAssetData } from "@/services/storage/types"; import type { MigrationResult, ProjectRecord } from "./types"; import { getProjectId, isRecord } from "./utils"; -interface LegacyTimelineData { - tracks: unknown[]; - lastModified: string; +// Frozen snapshots of v2-era defaults. See ./README.md. +const DEFAULT_BACKGROUND_BLUR_INTENSITY = 10; +const DEFAULT_BACKGROUND_COLOR = "#000000"; +const DEFAULT_CANVAS_SIZE = { width: 1920, height: 1080 }; +const DEFAULT_FPS = 30; + +type LegacyMediaType = "image" | "video" | "audio"; + +export interface V1ToV2Context { + legacyTracksBySceneId: Record; + mediaTypesById: Record; } +const EMPTY_V1_TO_V2_CONTEXT: V1ToV2Context = { + legacyTracksBySceneId: {}, + mediaTypesById: {}, +}; + interface LegacyMediaElement { type: "media"; mediaId: string; @@ -146,21 +153,13 @@ interface V2AudioTrack { type V2TimelineTrack = V2VideoTrack | V2TextTrack | V2AudioTrack; -export interface TransformV1ToV2Options { - loadMediaAsset?: ({ - mediaId, - }: { - mediaId: string; - }) => Promise; -} - -export async function transformProjectV1ToV2({ +export function transformProjectV1ToV2({ project, - options = {}, + context = EMPTY_V1_TO_V2_CONTEXT, }: { project: ProjectRecord; - options?: TransformV1ToV2Options; -}): Promise> { + context?: V1ToV2Context; +}): MigrationResult { const projectId = getProjectId({ project }); if (!projectId) { return { project, skipped: true, reason: "no project id" }; @@ -170,27 +169,23 @@ export async function transformProjectV1ToV2({ return { project, skipped: true, reason: "already v2" }; } - const migratedProject = await migrateProject({ + const migratedProject = migrateProject({ project, projectId, - loadMediaAsset: options.loadMediaAsset, + context, }); return { project: migratedProject, skipped: false }; } -async function migrateProject({ +function migrateProject({ project, projectId, - loadMediaAsset, + context, }: { project: ProjectRecord; projectId: string; - loadMediaAsset?: ({ - mediaId, - }: { - mediaId: string; - }) => Promise; -}): Promise { + context: V1ToV2Context; +}): ProjectRecord { const createdAt = normalizeDateString({ value: project.createdAt }); const updatedAt = normalizeDateString({ value: project.updatedAt }); const metadataValue = project.metadata; @@ -217,42 +212,35 @@ async function migrateProject({ ? project.bookmarks : null; - const migratedScenes = await Promise.all( - scenes.map(async (scene) => { - if (!isRecord(scene)) { - return scene; - } + const migratedScenes = scenes.map((scene) => { + if (!isRecord(scene)) { + return scene; + } - const sceneId = getStringValue({ value: scene.id }); - if (!sceneId) { - return scene; - } + const sceneId = getStringValue({ value: scene.id }); + if (!sceneId) { + return scene; + } - const existingTracks = scene.tracks; - const shouldLoadTracks = - !Array.isArray(existingTracks) || existingTracks.length === 0; + const existingTracks = scene.tracks; + const shouldLoadTracks = + !Array.isArray(existingTracks) || existingTracks.length === 0; - if (!shouldLoadTracks) { - return scene; - } + if (!shouldLoadTracks) { + return scene; + } - const tracks = await loadTracksFromLegacyDB({ - projectId, - sceneId, - isMain: scene.isMain === true, - }); + const tracks = context.legacyTracksBySceneId[sceneId] ?? []; + const transformedTracks = transformTracks({ + tracks, + context, + }); - const transformedTracks = await transformTracks({ - tracks, - loadMediaAsset, - }); - - return { - ...scene, - tracks: transformedTracks, - }; - }), - ); + return { + ...scene, + tracks: transformedTracks, + }; + }); const normalizedScenes = applyLegacyBookmarks({ scenes: migratedScenes, @@ -305,183 +293,126 @@ async function migrateProject({ }; } -async function loadTracksFromLegacyDB({ - projectId, - sceneId, - isMain, -}: { - projectId: string; - sceneId: string; - isMain: boolean; -}): Promise { - if (typeof indexedDB === "undefined") { - return []; - } - - const sceneDbName = `video-editor-timelines-${projectId}-${sceneId}`; - const projectDbName = `video-editor-timelines-${projectId}`; - - const adapter = new IndexedDBAdapter( - sceneDbName, - "timeline", - 1, - ); - - let data = await adapter.get("timeline"); - - if (!data && isMain) { - const projectAdapter = new IndexedDBAdapter( - projectDbName, - "timeline", - 1, - ); - data = await projectAdapter.get("timeline"); - } - - if (!data || !Array.isArray(data.tracks)) { - return []; - } - - return data.tracks; -} - -async function transformTracks({ +function transformTracks({ tracks, - loadMediaAsset, + context, }: { tracks: unknown[]; - loadMediaAsset?: ({ - mediaId, - }: { - mediaId: string; - }) => Promise; -}): Promise { + context: V1ToV2Context; +}): V2TimelineTrack[] { if (!Array.isArray(tracks)) { return []; } let isFirstVideoTrackFound = false; - const transformedTracks: (V2TimelineTrack | null)[] = []; - - for (const track of tracks) { + const transformedTracks = tracks.map((track): V2TimelineTrack | null => { if (!isRecord(track)) { - transformedTracks.push(null); - continue; + return null; } const trackType = track.type; if (trackType === "media") { - const videoTrack = await transformMediaTrack({ - track: track as LegacyMediaTrack, - loadMediaAsset, - isMain: !isFirstVideoTrackFound, - }); + const isMain = !isFirstVideoTrackFound; isFirstVideoTrackFound = true; - transformedTracks.push(videoTrack); - continue; + const videoTrack = transformMediaTrack({ + track: track as LegacyMediaTrack, + context, + isMain, + }); + return videoTrack; } if (trackType === "text") { - transformedTracks.push(transformTextTrack({ track })); - continue; + return transformTextTrack({ track }); } if (trackType === "audio") { - transformedTracks.push(transformAudioTrack({ track })); - continue; + return transformAudioTrack({ track }); } - transformedTracks.push(null); - } + return null; + }); return transformedTracks.filter( (track): track is V2TimelineTrack => track !== null, ); } -async function transformMediaTrack({ +function transformMediaTrack({ track, - loadMediaAsset, + context, isMain, }: { track: LegacyMediaTrack; - loadMediaAsset?: ({ - mediaId, - }: { - mediaId: string; - }) => Promise; + context: V1ToV2Context; isMain: boolean; -}): Promise { +}): V2VideoTrack { const elements = Array.isArray(track.elements) ? track.elements : []; - const transformedElements = await Promise.all( - elements.map(async (element) => { - if (!isRecord(element) || element.type !== "media") { - return null; - } + const transformedElements = elements.map((element) => { + if (!isRecord(element) || element.type !== "media") { + return null; + } - const mediaElement = element as LegacyMediaElement; - const mediaId = getStringValue({ value: mediaElement.mediaId }); - if (!mediaId) { - return null; - } + const mediaElement = element as LegacyMediaElement; + const mediaId = getStringValue({ value: mediaElement.mediaId }); + if (!mediaId) { + return null; + } - let mediaType: "video" | "image" = "video"; - if (loadMediaAsset) { - const mediaAsset = await loadMediaAsset({ mediaId }); - if (mediaAsset) { - mediaType = mediaAsset.type === "image" ? "image" : "video"; - } - } + let mediaType: "video" | "image" = "video"; + const storedMediaType = context.mediaTypesById[mediaId]; + if (storedMediaType) { + mediaType = storedMediaType === "image" ? "image" : "video"; + } - const defaultTransform: V2Transform = { - scale: 1, - position: { x: 0, y: 0 }, - rotate: 0, - }; + const defaultTransform: V2Transform = { + scale: 1, + position: { x: 0, y: 0 }, + rotate: 0, + }; - const muted = mediaElement.muted === true; + const muted = mediaElement.muted === true; - if (mediaType === "image") { - const imageElement: V2ImageElement = { - id: getStringValue({ value: element.id, fallback: "" }), - name: getStringValue({ value: element.name, fallback: "" }), - type: "image", - mediaId, - duration: getNumberValue({ value: element.duration, fallback: 0 }), - startTime: getNumberValue({ - value: element.startTime, - fallback: 0, - }), - trimStart: getNumberValue({ - value: element.trimStart, - fallback: 0, - }), - trimEnd: getNumberValue({ value: element.trimEnd, fallback: 0 }), - hidden: false, - transform: defaultTransform, - opacity: 1, - }; - return imageElement; - } - - const videoElement: V2VideoElement = { + if (mediaType === "image") { + const imageElement: V2ImageElement = { id: getStringValue({ value: element.id, fallback: "" }), name: getStringValue({ value: element.name, fallback: "" }), - type: "video", + type: "image", mediaId, - muted, + duration: getNumberValue({ value: element.duration, fallback: 0 }), + startTime: getNumberValue({ + value: element.startTime, + fallback: 0, + }), + trimStart: getNumberValue({ + value: element.trimStart, + fallback: 0, + }), + trimEnd: getNumberValue({ value: element.trimEnd, fallback: 0 }), hidden: false, transform: defaultTransform, opacity: 1, - duration: getNumberValue({ value: element.duration, fallback: 0 }), - startTime: getNumberValue({ value: element.startTime, fallback: 0 }), - trimStart: getNumberValue({ value: element.trimStart, fallback: 0 }), - trimEnd: getNumberValue({ value: element.trimEnd, fallback: 0 }), }; - return videoElement; - }), - ); + return imageElement; + } + + const videoElement: V2VideoElement = { + id: getStringValue({ value: element.id, fallback: "" }), + name: getStringValue({ value: element.name, fallback: "" }), + type: "video", + mediaId, + muted, + hidden: false, + transform: defaultTransform, + opacity: 1, + duration: getNumberValue({ value: element.duration, fallback: 0 }), + startTime: getNumberValue({ value: element.startTime, fallback: 0 }), + trimStart: getNumberValue({ value: element.trimStart, fallback: 0 }), + trimEnd: getNumberValue({ value: element.trimEnd, fallback: 0 }), + }; + return videoElement; + }); const validElements = transformedElements.filter( (element): element is V2VideoElement | V2ImageElement => element !== null, diff --git a/apps/web/src/services/storage/migrations/transformers/v14-to-v15.ts b/apps/web/src/services/storage/migrations/transformers/v14-to-v15.ts index 5f3c01e9..84b2704a 100644 --- a/apps/web/src/services/storage/migrations/transformers/v14-to-v15.ts +++ b/apps/web/src/services/storage/migrations/transformers/v14-to-v15.ts @@ -1,67 +1,69 @@ -import { STICKER_INTRINSIC_SIZE_FALLBACK } from "@/stickers/intrinsic-size"; -import type { MigrationResult, ProjectRecord } from "./types"; -import { getProjectId, isRecord } from "./utils"; - -export function transformProjectV14ToV15({ - project, -}: { - project: ProjectRecord; -}): MigrationResult { - if (!getProjectId({ project })) { - return { project, skipped: true, reason: "no project id" }; - } - - if (typeof project.version === "number" && project.version >= 15) { - return { project, skipped: true, reason: "already v15" }; - } - - const migratedProject = backfillStickerIntrinsicDimensions({ project }); - - return { - project: { ...migratedProject, version: 15 }, - skipped: false, - }; -} - -function backfillStickerIntrinsicDimensions({ - project, -}: { - project: ProjectRecord; -}): ProjectRecord { - const scenesValue = project.scenes; - if (!Array.isArray(scenesValue)) return project; - - const migratedScenes = scenesValue.map((scene) => { - if (!isRecord(scene)) return scene; - const tracksValue = scene.tracks; - if (!Array.isArray(tracksValue)) return scene; - - const migratedTracks = tracksValue.map((track) => { - if (!isRecord(track)) return track; - const elementsValue = track.elements; - if (!Array.isArray(elementsValue)) return track; - - const migratedElements = elementsValue.map((element) => { - if (!isRecord(element)) return element; - if (element.type !== "sticker") return element; - if ( - typeof element.intrinsicWidth === "number" && - typeof element.intrinsicHeight === "number" - ) { - return element; - } - return { - ...element, - intrinsicWidth: STICKER_INTRINSIC_SIZE_FALLBACK, - intrinsicHeight: STICKER_INTRINSIC_SIZE_FALLBACK, - }; - }); - - return { ...track, elements: migratedElements }; - }); - - return { ...scene, tracks: migratedTracks }; - }); - - return { ...project, scenes: migratedScenes }; -} +import type { MigrationResult, ProjectRecord } from "./types"; +import { getProjectId, isRecord } from "./utils"; + +// Frozen snapshot of the v15-era fallback. See ./README.md. +const STICKER_INTRINSIC_SIZE_FALLBACK = 200; + +export function transformProjectV14ToV15({ + project, +}: { + project: ProjectRecord; +}): MigrationResult { + if (!getProjectId({ project })) { + return { project, skipped: true, reason: "no project id" }; + } + + if (typeof project.version === "number" && project.version >= 15) { + return { project, skipped: true, reason: "already v15" }; + } + + const migratedProject = backfillStickerIntrinsicDimensions({ project }); + + return { + project: { ...migratedProject, version: 15 }, + skipped: false, + }; +} + +function backfillStickerIntrinsicDimensions({ + project, +}: { + project: ProjectRecord; +}): ProjectRecord { + const scenesValue = project.scenes; + if (!Array.isArray(scenesValue)) return project; + + const migratedScenes = scenesValue.map((scene) => { + if (!isRecord(scene)) return scene; + const tracksValue = scene.tracks; + if (!Array.isArray(tracksValue)) return scene; + + const migratedTracks = tracksValue.map((track) => { + if (!isRecord(track)) return track; + const elementsValue = track.elements; + if (!Array.isArray(elementsValue)) return track; + + const migratedElements = elementsValue.map((element) => { + if (!isRecord(element)) return element; + if (element.type !== "sticker") return element; + if ( + typeof element.intrinsicWidth === "number" && + typeof element.intrinsicHeight === "number" + ) { + return element; + } + return { + ...element, + intrinsicWidth: STICKER_INTRINSIC_SIZE_FALLBACK, + intrinsicHeight: STICKER_INTRINSIC_SIZE_FALLBACK, + }; + }); + + return { ...track, elements: migratedElements }; + }); + + return { ...scene, tracks: migratedTracks }; + }); + + return { ...project, scenes: migratedScenes }; +} diff --git a/apps/web/src/services/storage/migrations/transformers/v17-to-v18.ts b/apps/web/src/services/storage/migrations/transformers/v17-to-v18.ts index 3ef6cde9..660d91ee 100644 --- a/apps/web/src/services/storage/migrations/transformers/v17-to-v18.ts +++ b/apps/web/src/services/storage/migrations/transformers/v17-to-v18.ts @@ -1,105 +1,113 @@ -import type { MigrationResult, ProjectRecord } from "./types"; -import { VOLUME_DB_MIN } from "@/timeline/audio-constants"; -import { clampDb } from "@/timeline/audio-state"; -import { getProjectId, isRecord } from "./utils"; - -export function transformProjectV17ToV18({ - project, -}: { - project: ProjectRecord; -}): MigrationResult { - if (!getProjectId({ project })) { - return { project, skipped: true, reason: "no project id" }; - } - - if (typeof project.version === "number" && project.version >= 18) { - return { project, skipped: true, reason: "already v18" }; - } - - return { - project: { - ...migrateElementVolumes({ project }), - version: 18, - }, - skipped: false, - }; -} - -function migrateElementVolumes({ - project, -}: { - project: ProjectRecord; -}): ProjectRecord { - const scenesValue = project.scenes; - if (!Array.isArray(scenesValue)) { - return project; - } - - const migratedScenes = scenesValue.map((scene) => { - if (!isRecord(scene)) { - return scene; - } - - const tracksValue = scene.tracks; - if (!Array.isArray(tracksValue)) { - return scene; - } - - const migratedTracks = tracksValue.map((track) => { - if (!isRecord(track)) { - return track; - } - - const elementsValue = track.elements; - if (!Array.isArray(elementsValue)) { - return track; - } - - const migratedElements = elementsValue.map((element) => { - if (!isRecord(element)) { - return element; - } - - if (element.type !== "audio" && element.type !== "video") { - return element; - } - - const legacyVolume = element.volume; - return { - ...element, - volume: - typeof legacyVolume === "number" - ? linearGainToDb(legacyVolume) - : 0, - }; - }); - - return { - ...track, - elements: migratedElements, - }; - }); - - return { - ...scene, - tracks: migratedTracks, - }; - }); - - return { - ...project, - scenes: migratedScenes, - }; -} - -function linearGainToDb(gain: number): number { - if (!Number.isFinite(gain)) { - return 0; - } - - if (gain <= 0) { - return VOLUME_DB_MIN; - } - - return clampDb(20 * Math.log10(gain)); -} +import type { MigrationResult, ProjectRecord } from "./types"; +import { getProjectId, isRecord } from "./utils"; + +// Frozen snapshots of v18-era volume bounds. See ./README.md. +const VOLUME_DB_MIN = -60; +const VOLUME_DB_MAX = 20; + +export function transformProjectV17ToV18({ + project, +}: { + project: ProjectRecord; +}): MigrationResult { + if (!getProjectId({ project })) { + return { project, skipped: true, reason: "no project id" }; + } + + if (typeof project.version === "number" && project.version >= 18) { + return { project, skipped: true, reason: "already v18" }; + } + + return { + project: { + ...migrateElementVolumes({ project }), + version: 18, + }, + skipped: false, + }; +} + +function migrateElementVolumes({ + project, +}: { + project: ProjectRecord; +}): ProjectRecord { + const scenesValue = project.scenes; + if (!Array.isArray(scenesValue)) { + return project; + } + + const migratedScenes = scenesValue.map((scene) => { + if (!isRecord(scene)) { + return scene; + } + + const tracksValue = scene.tracks; + if (!Array.isArray(tracksValue)) { + return scene; + } + + const migratedTracks = tracksValue.map((track) => { + if (!isRecord(track)) { + return track; + } + + const elementsValue = track.elements; + if (!Array.isArray(elementsValue)) { + return track; + } + + const migratedElements = elementsValue.map((element) => { + if (!isRecord(element)) { + return element; + } + + if (element.type !== "audio" && element.type !== "video") { + return element; + } + + const legacyVolume = element.volume; + return { + ...element, + volume: + typeof legacyVolume === "number" ? linearGainToDb(legacyVolume) : 0, + }; + }); + + return { + ...track, + elements: migratedElements, + }; + }); + + return { + ...scene, + tracks: migratedTracks, + }; + }); + + return { + ...project, + scenes: migratedScenes, + }; +} + +function linearGainToDb(gain: number): number { + if (!Number.isFinite(gain)) { + return 0; + } + + if (gain <= 0) { + return VOLUME_DB_MIN; + } + + return clampDb(20 * Math.log10(gain)); +} + +function clampDb(value: number): number { + if (!Number.isFinite(value)) { + return 0; + } + + return Math.min(VOLUME_DB_MAX, Math.max(VOLUME_DB_MIN, value)); +} diff --git a/apps/web/src/services/storage/migrations/transformers/v20-to-v21.ts b/apps/web/src/services/storage/migrations/transformers/v20-to-v21.ts index 6c30e5c8..14b7a092 100644 --- a/apps/web/src/services/storage/migrations/transformers/v20-to-v21.ts +++ b/apps/web/src/services/storage/migrations/transformers/v20-to-v21.ts @@ -1,7 +1,8 @@ -import { INTENSITY_TO_SIGMA_DIVISOR } from "@/effects/definitions/blur"; import type { MigrationResult, ProjectRecord } from "./types"; import { getProjectId, isRecord } from "./utils"; +// Frozen snapshot of the v21-era divisor. See ./README.md. +const INTENSITY_TO_SIGMA_DIVISOR = 5; const LEGACY_DEFAULT_BACKGROUND_BLUR_INTENSITY = 50; export function transformProjectV20ToV21({ diff --git a/apps/web/src/services/storage/migrations/transformers/v22-to-v23.ts b/apps/web/src/services/storage/migrations/transformers/v22-to-v23.ts index 5e3adc33..24cc86e2 100644 --- a/apps/web/src/services/storage/migrations/transformers/v22-to-v23.ts +++ b/apps/web/src/services/storage/migrations/transformers/v22-to-v23.ts @@ -1,7 +1,8 @@ import type { MigrationResult, ProjectRecord } from "./types"; import { getProjectId, isRecord } from "./utils"; -import { TICKS_PER_SECOND } from "@/wasm"; +// Frozen snapshot of the v23-era tick rate. See ./README.md. +const TICKS_PER_SECOND = 120_000; const ARBITRARY_FPS_DENOMINATOR = 1_000_000; const STANDARD_FRAME_RATES = [ { value: 24_000 / 1_001, numerator: 24_000, denominator: 1_001 }, @@ -193,7 +194,9 @@ function migrateAnimationChannel({ channel }: { channel: unknown }): unknown { return { ...channel, - keys: channel.keys.map((keyframe) => migrateAnimationKeyframe({ keyframe })), + keys: channel.keys.map((keyframe) => + migrateAnimationKeyframe({ keyframe }), + ), }; } @@ -290,7 +293,8 @@ function migrateFrameRate({ fps }: { fps: unknown }): unknown { } const standardFrameRate = STANDARD_FRAME_RATES.find( - (candidate) => Math.abs(fps - candidate.value) <= STANDARD_FRAME_RATE_TOLERANCE, + (candidate) => + Math.abs(fps - candidate.value) <= STANDARD_FRAME_RATE_TOLERANCE, ); if (standardFrameRate) { return { 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 820b0087..3b118ee7 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 @@ -1,7 +1,50 @@ -import { parseCustomMaskPath } from "@/masks/custom-path"; import type { MigrationResult, ProjectRecord } from "./types"; import { getProjectId, isRecord } from "./utils"; +interface CustomMaskPathPoint { + id: string; + x: number; + y: number; + inX: number; + inY: number; + outX: number; + outY: number; +} + +function isCustomMaskPathPoint(value: unknown): value is CustomMaskPathPoint { + if (!value || typeof value !== "object") { + return false; + } + + const candidate = value as Record; + 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" + ); +} + +function parseCustomMaskPath({ + path, +}: { + path: string; +}): CustomMaskPathPoint[] { + if (!path) { + return []; + } + + try { + const parsed = JSON.parse(path); + return Array.isArray(parsed) ? parsed.filter(isCustomMaskPathPoint) : []; + } catch { + return []; + } +} + export function transformProjectV26ToV27({ project, }: { diff --git a/apps/web/src/services/storage/migrations/v0-to-v1.ts b/apps/web/src/services/storage/migrations/v0-to-v1.ts index 8868e3f2..bebd0103 100644 --- a/apps/web/src/services/storage/migrations/v0-to-v1.ts +++ b/apps/web/src/services/storage/migrations/v0-to-v1.ts @@ -1,16 +1,14 @@ -import { StorageMigration } from "./base"; -import type { ProjectRecord } from "./transformers/types"; +import { StorageMigration, type StorageMigrationRunArgs } from "./base"; +import type { MigrationResult, ProjectRecord } from "./transformers/types"; import { transformProjectV0ToV1 } from "./transformers/v0-to-v1"; export class V0toV1Migration extends StorageMigration { from = 0; to = 1; - async transform(project: ProjectRecord): Promise<{ - project: ProjectRecord; - skipped: boolean; - reason?: string; - }> { + async run({ + project, + }: StorageMigrationRunArgs): Promise> { return transformProjectV0ToV1({ project }); } } diff --git a/apps/web/src/services/storage/migrations/v1-to-v2.ts b/apps/web/src/services/storage/migrations/v1-to-v2.ts index ef381541..a87b4a86 100644 --- a/apps/web/src/services/storage/migrations/v1-to-v2.ts +++ b/apps/web/src/services/storage/migrations/v1-to-v2.ts @@ -3,97 +3,260 @@ import { deleteDatabase, } from "@/services/storage/indexeddb-adapter"; import type { MediaAssetData } from "@/services/storage/types"; -import { StorageMigration } from "./base"; -import type { ProjectRecord } from "./transformers/types"; +import { StorageMigration, type StorageMigrationRunArgs } from "./base"; +import type { MigrationResult, ProjectRecord } from "./transformers/types"; import { - getProjectId, transformProjectV1ToV2, - type TransformV1ToV2Options, + type V1ToV2Context, } from "./transformers/v1-to-v2"; +import { isRecord } from "./transformers/utils"; + +interface LegacyTimelineData { + tracks: unknown[]; + lastModified: string; +} export class V1toV2Migration extends StorageMigration { from = 1; to = 2; - async transform(project: ProjectRecord): Promise<{ - project: ProjectRecord; - skipped: boolean; - reason?: string; - }> { - const projectId = getProjectId({ project }); - if (!projectId) { - return { project, skipped: true, reason: "no project id" }; - } - - const loadMediaAsset = createMediaAssetLoader({ projectId }); - - const result = await transformProjectV1ToV2({ - project, - options: { loadMediaAsset }, - }); + async run({ + projectId, + project, + }: StorageMigrationRunArgs): Promise> { + const context = await loadV1ToV2Context({ projectId, project }); + const result = transformProjectV1ToV2({ project, context }); if (!result.skipped) { - void cleanupLegacyTimelineDBs({ - projectId, - project: result.project, - }); + try { + await deleteLegacyTimelineDbs({ projectId, project: result.project }); + } catch (error) { + console.error( + `V1→V2 migration cleanup failed for project ${projectId}:`, + error, + ); + } } return result; } } -function createMediaAssetLoader({ - projectId, -}: { - projectId: string; -}): TransformV1ToV2Options["loadMediaAsset"] { - return async ({ mediaId }: { mediaId: string }) => { - const mediaMetadataAdapter = new IndexedDBAdapter( - `video-editor-media-${projectId}`, - "media-metadata", - 1, - ); - - return mediaMetadataAdapter.get(mediaId); - }; -} - -function cleanupLegacyTimelineDBs({ +async function loadV1ToV2Context({ projectId, project, }: { projectId: string; project: ProjectRecord; -}): void { +}): Promise { + const legacyTracksBySceneId = await loadLegacyTracksBySceneId({ + projectId, + project, + }); + const mediaTypesById = await loadMediaTypesById({ + projectId, + legacyTracksBySceneId, + }); + + return { + legacyTracksBySceneId, + mediaTypesById, + }; +} + +async function loadLegacyTracksBySceneId({ + projectId, + project, +}: { + projectId: string; + project: ProjectRecord; +}): Promise { const scenes = project.scenes; if (!Array.isArray(scenes)) { - return; + return {}; } - const dbNamesToDelete: string[] = []; + const sceneEntries = await Promise.all( + scenes.map(async (scene) => { + if (!isRecord(scene)) { + return null; + } - for (const scene of scenes) { - if (typeof scene !== "object" || scene === null) { - continue; - } + const sceneId = scene.id; + if (typeof sceneId !== "string") { + return null; + } - const sceneId = scene.id; - if (typeof sceneId === "string") { - const sceneDbName = `video-editor-timelines-${projectId}-${sceneId}`; - dbNamesToDelete.push(sceneDbName); - } - } + const tracks = await loadLegacyTracksForScene({ + projectId, + sceneId, + isMain: scene.isMain === true, + }); - const projectDbName = `video-editor-timelines-${projectId}`; - dbNamesToDelete.push(projectDbName); + return [sceneId, tracks] as const; + }), + ); - // Fire-and-forget: delete in parallel, don't block migration - void Promise.all( - dbNamesToDelete.map((dbName) => - deleteDatabase({ dbName }).catch(() => { - // ignore errors, DB might not exist or already deleted - }), + return Object.fromEntries( + sceneEntries.filter( + ( + sceneEntry, + ): sceneEntry is readonly [ + string, + V1ToV2Context["legacyTracksBySceneId"][string], + ] => sceneEntry !== null, ), ); } + +async function loadLegacyTracksForScene({ + projectId, + sceneId, + isMain, +}: { + projectId: string; + sceneId: string; + isMain: boolean; +}): Promise { + const sceneDbName = `video-editor-timelines-${projectId}-${sceneId}`; + const projectDbName = `video-editor-timelines-${projectId}`; + + const adapter = new IndexedDBAdapter( + sceneDbName, + "timeline", + 1, + ); + + let data = await adapter.get("timeline"); + + if (!data && isMain) { + const projectAdapter = new IndexedDBAdapter( + projectDbName, + "timeline", + 1, + ); + data = await projectAdapter.get("timeline"); + } + + if (!data || !Array.isArray(data.tracks)) { + return []; + } + + return data.tracks; +} + +async function loadMediaTypesById({ + projectId, + legacyTracksBySceneId, +}: { + projectId: string; + legacyTracksBySceneId: V1ToV2Context["legacyTracksBySceneId"]; +}): Promise { + const mediaIds = collectLegacyMediaIds({ legacyTracksBySceneId }); + if (mediaIds.length === 0) { + return {}; + } + + const mediaMetadataAdapter = new IndexedDBAdapter( + `video-editor-media-${projectId}`, + "media-metadata", + 1, + ); + + const mediaEntries = await Promise.all( + mediaIds.map(async (mediaId) => { + const mediaMetadata = await mediaMetadataAdapter.get(mediaId); + if (!mediaMetadata) { + return null; + } + + return [mediaId, mediaMetadata.type] as const; + }), + ); + + return Object.fromEntries( + mediaEntries.filter( + ( + mediaEntry, + ): mediaEntry is readonly [ + string, + V1ToV2Context["mediaTypesById"][string], + ] => mediaEntry !== null, + ), + ); +} + +function collectLegacyMediaIds({ + legacyTracksBySceneId, +}: { + legacyTracksBySceneId: V1ToV2Context["legacyTracksBySceneId"]; +}): string[] { + const mediaIds = new Set(); + + for (const tracks of Object.values(legacyTracksBySceneId)) { + if (!Array.isArray(tracks)) { + continue; + } + + for (const track of tracks) { + if (!isRecord(track) || track.type !== "media") { + continue; + } + + const elements = track.elements; + if (!Array.isArray(elements)) { + continue; + } + + for (const element of elements) { + if (!isRecord(element) || element.type !== "media") { + continue; + } + + if (typeof element.mediaId !== "string") { + continue; + } + + mediaIds.add(element.mediaId); + } + } + } + + return Array.from(mediaIds); +} + +async function deleteLegacyTimelineDbs({ + projectId, + project, +}: { + projectId: string; + project: ProjectRecord; +}): Promise { + const dbNames = getLegacyTimelineDbNames({ projectId, project }); + await Promise.all(dbNames.map((dbName) => deleteDatabase({ dbName }))); +} + +function getLegacyTimelineDbNames({ + projectId, + project, +}: { + projectId: string; + project: ProjectRecord; +}): string[] { + const scenes = project.scenes; + if (!Array.isArray(scenes)) { + return [`video-editor-timelines-${projectId}`]; + } + + const sceneDbNames = scenes.flatMap((scene) => { + if (!isRecord(scene)) { + return []; + } + + return typeof scene.id === "string" + ? [`video-editor-timelines-${projectId}-${scene.id}`] + : []; + }); + + return [...sceneDbNames, `video-editor-timelines-${projectId}`]; +} diff --git a/apps/web/src/services/storage/migrations/v10-to-v11.ts b/apps/web/src/services/storage/migrations/v10-to-v11.ts index 345dbeb3..53ce5bc5 100644 --- a/apps/web/src/services/storage/migrations/v10-to-v11.ts +++ b/apps/web/src/services/storage/migrations/v10-to-v11.ts @@ -1,16 +1,14 @@ -import { StorageMigration } from "./base"; -import type { ProjectRecord } from "./transformers/types"; +import { StorageMigration, type StorageMigrationRunArgs } from "./base"; +import type { MigrationResult, ProjectRecord } from "./transformers/types"; import { transformProjectV10ToV11 } from "./transformers/v10-to-v11"; export class V10toV11Migration extends StorageMigration { from = 10; to = 11; - async transform(project: ProjectRecord): Promise<{ - project: ProjectRecord; - skipped: boolean; - reason?: string; - }> { + async run({ + project, + }: StorageMigrationRunArgs): Promise> { return transformProjectV10ToV11({ project }); } } diff --git a/apps/web/src/services/storage/migrations/v11-to-v12.ts b/apps/web/src/services/storage/migrations/v11-to-v12.ts index b3154ca8..df626f2f 100644 --- a/apps/web/src/services/storage/migrations/v11-to-v12.ts +++ b/apps/web/src/services/storage/migrations/v11-to-v12.ts @@ -1,16 +1,14 @@ -import { StorageMigration } from "./base"; -import type { ProjectRecord } from "./transformers/types"; +import { StorageMigration, type StorageMigrationRunArgs } from "./base"; +import type { MigrationResult, ProjectRecord } from "./transformers/types"; import { transformProjectV11ToV12 } from "./transformers/v11-to-v12"; export class V11toV12Migration extends StorageMigration { from = 11; to = 12; - async transform(project: ProjectRecord): Promise<{ - project: ProjectRecord; - skipped: boolean; - reason?: string; - }> { + async run({ + project, + }: StorageMigrationRunArgs): Promise> { return transformProjectV11ToV12({ project }); } } diff --git a/apps/web/src/services/storage/migrations/v12-to-v13.ts b/apps/web/src/services/storage/migrations/v12-to-v13.ts index 34b8de60..466efb5f 100644 --- a/apps/web/src/services/storage/migrations/v12-to-v13.ts +++ b/apps/web/src/services/storage/migrations/v12-to-v13.ts @@ -1,16 +1,14 @@ -import { StorageMigration } from "./base"; -import type { ProjectRecord } from "./transformers/types"; +import { StorageMigration, type StorageMigrationRunArgs } from "./base"; +import type { MigrationResult, ProjectRecord } from "./transformers/types"; import { transformProjectV12ToV13 } from "./transformers/v12-to-v13"; export class V12toV13Migration extends StorageMigration { from = 12; to = 13; - async transform(project: ProjectRecord): Promise<{ - project: ProjectRecord; - skipped: boolean; - reason?: string; - }> { + async run({ + project, + }: StorageMigrationRunArgs): Promise> { return transformProjectV12ToV13({ project }); } } diff --git a/apps/web/src/services/storage/migrations/v13-to-v14.ts b/apps/web/src/services/storage/migrations/v13-to-v14.ts index b8bde59c..5b26f59a 100644 --- a/apps/web/src/services/storage/migrations/v13-to-v14.ts +++ b/apps/web/src/services/storage/migrations/v13-to-v14.ts @@ -1,16 +1,14 @@ -import { StorageMigration } from "./base"; -import type { ProjectRecord } from "./transformers/types"; +import { StorageMigration, type StorageMigrationRunArgs } from "./base"; +import type { MigrationResult, ProjectRecord } from "./transformers/types"; import { transformProjectV13ToV14 } from "./transformers/v13-to-v14"; export class V13toV14Migration extends StorageMigration { from = 13; to = 14; - async transform(project: ProjectRecord): Promise<{ - project: ProjectRecord; - skipped: boolean; - reason?: string; - }> { + async run({ + project, + }: StorageMigrationRunArgs): Promise> { return transformProjectV13ToV14({ project }); } } diff --git a/apps/web/src/services/storage/migrations/v14-to-v15.ts b/apps/web/src/services/storage/migrations/v14-to-v15.ts index 5c3a60cf..d2b58ab8 100644 --- a/apps/web/src/services/storage/migrations/v14-to-v15.ts +++ b/apps/web/src/services/storage/migrations/v14-to-v15.ts @@ -1,16 +1,14 @@ -import { StorageMigration } from "./base"; -import type { ProjectRecord } from "./transformers/types"; +import { StorageMigration, type StorageMigrationRunArgs } from "./base"; +import type { MigrationResult, ProjectRecord } from "./transformers/types"; import { transformProjectV14ToV15 } from "./transformers/v14-to-v15"; export class V14toV15Migration extends StorageMigration { from = 14; to = 15; - async transform(project: ProjectRecord): Promise<{ - project: ProjectRecord; - skipped: boolean; - reason?: string; - }> { + async run({ + project, + }: StorageMigrationRunArgs): Promise> { return transformProjectV14ToV15({ project }); } } diff --git a/apps/web/src/services/storage/migrations/v15-to-v16.ts b/apps/web/src/services/storage/migrations/v15-to-v16.ts index 07afe148..221bf278 100644 --- a/apps/web/src/services/storage/migrations/v15-to-v16.ts +++ b/apps/web/src/services/storage/migrations/v15-to-v16.ts @@ -1,16 +1,14 @@ -import { StorageMigration } from "./base"; -import type { ProjectRecord } from "./transformers/types"; +import { StorageMigration, type StorageMigrationRunArgs } from "./base"; +import type { MigrationResult, ProjectRecord } from "./transformers/types"; import { transformProjectV15ToV16 } from "./transformers/v15-to-v16"; export class V15toV16Migration extends StorageMigration { from = 15; to = 16; - async transform(project: ProjectRecord): Promise<{ - project: ProjectRecord; - skipped: boolean; - reason?: string; - }> { + async run({ + project, + }: StorageMigrationRunArgs): Promise> { return transformProjectV15ToV16({ project }); } } diff --git a/apps/web/src/services/storage/migrations/v16-to-v17.ts b/apps/web/src/services/storage/migrations/v16-to-v17.ts index 053e8d07..367fa3a5 100644 --- a/apps/web/src/services/storage/migrations/v16-to-v17.ts +++ b/apps/web/src/services/storage/migrations/v16-to-v17.ts @@ -1,16 +1,14 @@ -import { StorageMigration } from "./base"; -import type { ProjectRecord } from "./transformers/types"; +import { StorageMigration, type StorageMigrationRunArgs } from "./base"; +import type { MigrationResult, ProjectRecord } from "./transformers/types"; import { transformProjectV16ToV17 } from "./transformers/v16-to-v17"; export class V16toV17Migration extends StorageMigration { from = 16; to = 17; - async transform(project: ProjectRecord): Promise<{ - project: ProjectRecord; - skipped: boolean; - reason?: string; - }> { + async run({ + project, + }: StorageMigrationRunArgs): Promise> { return transformProjectV16ToV17({ project }); } } diff --git a/apps/web/src/services/storage/migrations/v17-to-v18.ts b/apps/web/src/services/storage/migrations/v17-to-v18.ts index b1591907..142c7836 100644 --- a/apps/web/src/services/storage/migrations/v17-to-v18.ts +++ b/apps/web/src/services/storage/migrations/v17-to-v18.ts @@ -1,16 +1,14 @@ -import { StorageMigration } from "./base"; -import type { ProjectRecord } from "./transformers/types"; +import { StorageMigration, type StorageMigrationRunArgs } from "./base"; +import type { MigrationResult, ProjectRecord } from "./transformers/types"; import { transformProjectV17ToV18 } from "./transformers/v17-to-v18"; export class V17toV18Migration extends StorageMigration { from = 17; to = 18; - async transform(project: ProjectRecord): Promise<{ - project: ProjectRecord; - skipped: boolean; - reason?: string; - }> { + async run({ + project, + }: StorageMigrationRunArgs): Promise> { return transformProjectV17ToV18({ project }); } } diff --git a/apps/web/src/services/storage/migrations/v18-to-v19.ts b/apps/web/src/services/storage/migrations/v18-to-v19.ts index d8fde783..fdde8295 100644 --- a/apps/web/src/services/storage/migrations/v18-to-v19.ts +++ b/apps/web/src/services/storage/migrations/v18-to-v19.ts @@ -1,16 +1,14 @@ -import { StorageMigration } from "./base"; -import type { ProjectRecord } from "./transformers/types"; +import { StorageMigration, type StorageMigrationRunArgs } from "./base"; +import type { MigrationResult, ProjectRecord } from "./transformers/types"; import { transformProjectV18ToV19 } from "./transformers/v18-to-v19"; export class V18toV19Migration extends StorageMigration { from = 18; to = 19; - async transform(project: ProjectRecord): Promise<{ - project: ProjectRecord; - skipped: boolean; - reason?: string; - }> { + async run({ + project, + }: StorageMigrationRunArgs): Promise> { return transformProjectV18ToV19({ project }); } } diff --git a/apps/web/src/services/storage/migrations/v19-to-v20.ts b/apps/web/src/services/storage/migrations/v19-to-v20.ts index c6809067..c423e00b 100644 --- a/apps/web/src/services/storage/migrations/v19-to-v20.ts +++ b/apps/web/src/services/storage/migrations/v19-to-v20.ts @@ -1,16 +1,14 @@ -import { StorageMigration } from "./base"; -import type { ProjectRecord } from "./transformers/types"; +import { StorageMigration, type StorageMigrationRunArgs } from "./base"; +import type { MigrationResult, ProjectRecord } from "./transformers/types"; import { transformProjectV19ToV20 } from "./transformers/v19-to-v20"; export class V19toV20Migration extends StorageMigration { from = 19; to = 20; - async transform(project: ProjectRecord): Promise<{ - project: ProjectRecord; - skipped: boolean; - reason?: string; - }> { + async run({ + project, + }: StorageMigrationRunArgs): Promise> { return transformProjectV19ToV20({ project }); } } diff --git a/apps/web/src/services/storage/migrations/v2-to-v3.ts b/apps/web/src/services/storage/migrations/v2-to-v3.ts index 70eb9aad..b825bfc1 100644 --- a/apps/web/src/services/storage/migrations/v2-to-v3.ts +++ b/apps/web/src/services/storage/migrations/v2-to-v3.ts @@ -1,16 +1,14 @@ -import { StorageMigration } from "./base"; -import type { ProjectRecord } from "./transformers/types"; +import { StorageMigration, type StorageMigrationRunArgs } from "./base"; +import type { MigrationResult, ProjectRecord } from "./transformers/types"; import { transformProjectV2ToV3 } from "./transformers/v2-to-v3"; export class V2toV3Migration extends StorageMigration { from = 2; to = 3; - async transform(project: ProjectRecord): Promise<{ - project: ProjectRecord; - skipped: boolean; - reason?: string; - }> { + async run({ + project, + }: StorageMigrationRunArgs): Promise> { return transformProjectV2ToV3({ project }); } } diff --git a/apps/web/src/services/storage/migrations/v20-to-v21.ts b/apps/web/src/services/storage/migrations/v20-to-v21.ts index f04ad35f..f28837f1 100644 --- a/apps/web/src/services/storage/migrations/v20-to-v21.ts +++ b/apps/web/src/services/storage/migrations/v20-to-v21.ts @@ -1,16 +1,14 @@ -import { StorageMigration } from "./base"; -import type { ProjectRecord } from "./transformers/types"; +import { StorageMigration, type StorageMigrationRunArgs } from "./base"; +import type { MigrationResult, ProjectRecord } from "./transformers/types"; import { transformProjectV20ToV21 } from "./transformers/v20-to-v21"; export class V20toV21Migration extends StorageMigration { from = 20; to = 21; - async transform(project: ProjectRecord): Promise<{ - project: ProjectRecord; - skipped: boolean; - reason?: string; - }> { + async run({ + project, + }: StorageMigrationRunArgs): Promise> { return transformProjectV20ToV21({ project }); } } diff --git a/apps/web/src/services/storage/migrations/v21-to-v22.ts b/apps/web/src/services/storage/migrations/v21-to-v22.ts index 212109a5..667e14e2 100644 --- a/apps/web/src/services/storage/migrations/v21-to-v22.ts +++ b/apps/web/src/services/storage/migrations/v21-to-v22.ts @@ -1,16 +1,14 @@ -import { StorageMigration } from "./base"; -import type { ProjectRecord } from "./transformers/types"; +import { StorageMigration, type StorageMigrationRunArgs } from "./base"; +import type { MigrationResult, ProjectRecord } from "./transformers/types"; import { transformProjectV21ToV22 } from "./transformers/v21-to-v22"; export class V21toV22Migration extends StorageMigration { from = 21; to = 22; - async transform(project: ProjectRecord): Promise<{ - project: ProjectRecord; - skipped: boolean; - reason?: string; - }> { + async run({ + project, + }: StorageMigrationRunArgs): Promise> { return transformProjectV21ToV22({ project }); } } diff --git a/apps/web/src/services/storage/migrations/v22-to-v23.ts b/apps/web/src/services/storage/migrations/v22-to-v23.ts index cd352e45..36b335ff 100644 --- a/apps/web/src/services/storage/migrations/v22-to-v23.ts +++ b/apps/web/src/services/storage/migrations/v22-to-v23.ts @@ -1,16 +1,14 @@ -import { StorageMigration } from "./base"; -import type { ProjectRecord } from "./transformers/types"; +import { StorageMigration, type StorageMigrationRunArgs } from "./base"; +import type { MigrationResult, ProjectRecord } from "./transformers/types"; import { transformProjectV22ToV23 } from "./transformers/v22-to-v23"; export class V22toV23Migration extends StorageMigration { from = 22; to = 23; - async transform(project: ProjectRecord): Promise<{ - project: ProjectRecord; - skipped: boolean; - reason?: string; - }> { + async run({ + project, + }: StorageMigrationRunArgs): Promise> { return transformProjectV22ToV23({ project }); } } diff --git a/apps/web/src/services/storage/migrations/v23-to-v24.ts b/apps/web/src/services/storage/migrations/v23-to-v24.ts index fe952bb1..3825a9eb 100644 --- a/apps/web/src/services/storage/migrations/v23-to-v24.ts +++ b/apps/web/src/services/storage/migrations/v23-to-v24.ts @@ -1,16 +1,14 @@ -import { StorageMigration } from "./base"; -import type { ProjectRecord } from "./transformers/types"; +import { StorageMigration, type StorageMigrationRunArgs } from "./base"; +import type { MigrationResult, ProjectRecord } from "./transformers/types"; import { transformProjectV23ToV24 } from "./transformers/v23-to-v24"; export class V23toV24Migration extends StorageMigration { from = 23; to = 24; - async transform(project: ProjectRecord): Promise<{ - project: ProjectRecord; - skipped: boolean; - reason?: string; - }> { + async run({ + project, + }: StorageMigrationRunArgs): Promise> { return transformProjectV23ToV24({ project }); } } diff --git a/apps/web/src/services/storage/migrations/v24-to-v25.ts b/apps/web/src/services/storage/migrations/v24-to-v25.ts index 2e2a6935..5190b129 100644 --- a/apps/web/src/services/storage/migrations/v24-to-v25.ts +++ b/apps/web/src/services/storage/migrations/v24-to-v25.ts @@ -1,16 +1,14 @@ -import { StorageMigration } from "./base"; -import type { ProjectRecord } from "./transformers/types"; +import { StorageMigration, type StorageMigrationRunArgs } from "./base"; +import type { MigrationResult, ProjectRecord } from "./transformers/types"; import { transformProjectV24ToV25 } from "./transformers/v24-to-v25"; export class V24toV25Migration extends StorageMigration { from = 24; to = 25; - async transform(project: ProjectRecord): Promise<{ - project: ProjectRecord; - skipped: boolean; - reason?: string; - }> { + async run({ + project, + }: StorageMigrationRunArgs): Promise> { return transformProjectV24ToV25({ project }); } } diff --git a/apps/web/src/services/storage/migrations/v25-to-v26.ts b/apps/web/src/services/storage/migrations/v25-to-v26.ts index b5a8f310..8674d84b 100644 --- a/apps/web/src/services/storage/migrations/v25-to-v26.ts +++ b/apps/web/src/services/storage/migrations/v25-to-v26.ts @@ -1,16 +1,14 @@ -import { StorageMigration } from "./base"; -import type { ProjectRecord } from "./transformers/types"; +import { StorageMigration, type StorageMigrationRunArgs } from "./base"; +import type { MigrationResult, ProjectRecord } from "./transformers/types"; import { transformProjectV25ToV26 } from "./transformers/v25-to-v26"; export class V25toV26Migration extends StorageMigration { from = 25; to = 26; - async transform(project: ProjectRecord): Promise<{ - project: ProjectRecord; - skipped: boolean; - reason?: string; - }> { + async run({ + project, + }: StorageMigrationRunArgs): Promise> { return transformProjectV25ToV26({ project }); } } diff --git a/apps/web/src/services/storage/migrations/v26-to-v27.ts b/apps/web/src/services/storage/migrations/v26-to-v27.ts index 2df5329d..92ef18c0 100644 --- a/apps/web/src/services/storage/migrations/v26-to-v27.ts +++ b/apps/web/src/services/storage/migrations/v26-to-v27.ts @@ -1,16 +1,14 @@ -import { StorageMigration } from "./base"; -import type { ProjectRecord } from "./transformers/types"; +import { StorageMigration, type StorageMigrationRunArgs } from "./base"; +import type { MigrationResult, ProjectRecord } from "./transformers/types"; import { transformProjectV26ToV27 } from "./transformers/v26-to-v27"; export class V26toV27Migration extends StorageMigration { from = 26; to = 27; - async transform(project: ProjectRecord): Promise<{ - project: ProjectRecord; - skipped: boolean; - reason?: string; - }> { + async run({ + project, + }: StorageMigrationRunArgs): Promise> { return transformProjectV26ToV27({ project }); } } diff --git a/apps/web/src/services/storage/migrations/v3-to-v4.ts b/apps/web/src/services/storage/migrations/v3-to-v4.ts index a3738076..c8054f25 100644 --- a/apps/web/src/services/storage/migrations/v3-to-v4.ts +++ b/apps/web/src/services/storage/migrations/v3-to-v4.ts @@ -1,16 +1,14 @@ -import { StorageMigration } from "./base"; -import type { ProjectRecord } from "./transformers/types"; +import { StorageMigration, type StorageMigrationRunArgs } from "./base"; +import type { MigrationResult, ProjectRecord } from "./transformers/types"; import { transformProjectV3ToV4 } from "./transformers/v3-to-v4"; export class V3toV4Migration extends StorageMigration { from = 3; to = 4; - async transform(project: ProjectRecord): Promise<{ - project: ProjectRecord; - skipped: boolean; - reason?: string; - }> { + async run({ + project, + }: StorageMigrationRunArgs): Promise> { return transformProjectV3ToV4({ project }); } } diff --git a/apps/web/src/services/storage/migrations/v4-to-v5.ts b/apps/web/src/services/storage/migrations/v4-to-v5.ts index 394406d1..e8f27657 100644 --- a/apps/web/src/services/storage/migrations/v4-to-v5.ts +++ b/apps/web/src/services/storage/migrations/v4-to-v5.ts @@ -1,16 +1,14 @@ -import { StorageMigration } from "./base"; -import type { ProjectRecord } from "./transformers/types"; +import { StorageMigration, type StorageMigrationRunArgs } from "./base"; +import type { MigrationResult, ProjectRecord } from "./transformers/types"; import { transformProjectV4ToV5 } from "./transformers/v4-to-v5"; export class V4toV5Migration extends StorageMigration { from = 4; to = 5; - async transform(project: ProjectRecord): Promise<{ - project: ProjectRecord; - skipped: boolean; - reason?: string; - }> { + async run({ + project, + }: StorageMigrationRunArgs): Promise> { return transformProjectV4ToV5({ project }); } } diff --git a/apps/web/src/services/storage/migrations/v5-to-v6.ts b/apps/web/src/services/storage/migrations/v5-to-v6.ts index e6be4f9b..71061855 100644 --- a/apps/web/src/services/storage/migrations/v5-to-v6.ts +++ b/apps/web/src/services/storage/migrations/v5-to-v6.ts @@ -1,16 +1,14 @@ -import { StorageMigration } from "./base"; -import type { ProjectRecord } from "./transformers/types"; +import { StorageMigration, type StorageMigrationRunArgs } from "./base"; +import type { MigrationResult, ProjectRecord } from "./transformers/types"; import { transformProjectV5ToV6 } from "./transformers/v5-to-v6"; export class V5toV6Migration extends StorageMigration { from = 5; to = 6; - async transform(project: ProjectRecord): Promise<{ - project: ProjectRecord; - skipped: boolean; - reason?: string; - }> { + async run({ + project, + }: StorageMigrationRunArgs): Promise> { return transformProjectV5ToV6({ project }); } } diff --git a/apps/web/src/services/storage/migrations/v6-to-v7.ts b/apps/web/src/services/storage/migrations/v6-to-v7.ts index 3f63770b..4c917ed9 100644 --- a/apps/web/src/services/storage/migrations/v6-to-v7.ts +++ b/apps/web/src/services/storage/migrations/v6-to-v7.ts @@ -1,16 +1,14 @@ -import { StorageMigration } from "./base"; -import type { ProjectRecord } from "./transformers/types"; +import { StorageMigration, type StorageMigrationRunArgs } from "./base"; +import type { MigrationResult, ProjectRecord } from "./transformers/types"; import { transformProjectV6ToV7 } from "./transformers/v6-to-v7"; export class V6toV7Migration extends StorageMigration { from = 6; to = 7; - async transform(project: ProjectRecord): Promise<{ - project: ProjectRecord; - skipped: boolean; - reason?: string; - }> { + async run({ + project, + }: StorageMigrationRunArgs): Promise> { return transformProjectV6ToV7({ project }); } } diff --git a/apps/web/src/services/storage/migrations/v7-to-v8.ts b/apps/web/src/services/storage/migrations/v7-to-v8.ts index 5a3302f7..24de80c6 100644 --- a/apps/web/src/services/storage/migrations/v7-to-v8.ts +++ b/apps/web/src/services/storage/migrations/v7-to-v8.ts @@ -1,16 +1,14 @@ -import { StorageMigration } from "./base"; -import type { ProjectRecord } from "./transformers/types"; +import { StorageMigration, type StorageMigrationRunArgs } from "./base"; +import type { MigrationResult, ProjectRecord } from "./transformers/types"; import { transformProjectV7ToV8 } from "./transformers/v7-to-v8"; export class V7toV8Migration extends StorageMigration { from = 7; to = 8; - async transform(project: ProjectRecord): Promise<{ - project: ProjectRecord; - skipped: boolean; - reason?: string; - }> { + async run({ + project, + }: StorageMigrationRunArgs): Promise> { return transformProjectV7ToV8({ project }); } } diff --git a/apps/web/src/services/storage/migrations/v8-to-v9.ts b/apps/web/src/services/storage/migrations/v8-to-v9.ts index bdbed910..b8baee60 100644 --- a/apps/web/src/services/storage/migrations/v8-to-v9.ts +++ b/apps/web/src/services/storage/migrations/v8-to-v9.ts @@ -1,16 +1,14 @@ -import { StorageMigration } from "./base"; -import type { ProjectRecord } from "./transformers/types"; +import { StorageMigration, type StorageMigrationRunArgs } from "./base"; +import type { MigrationResult, ProjectRecord } from "./transformers/types"; import { transformProjectV8ToV9 } from "./transformers/v8-to-v9"; export class V8toV9Migration extends StorageMigration { from = 8; to = 9; - async transform(project: ProjectRecord): Promise<{ - project: ProjectRecord; - skipped: boolean; - reason?: string; - }> { + async run({ + project, + }: StorageMigrationRunArgs): Promise> { return transformProjectV8ToV9({ project }); } } diff --git a/apps/web/src/services/storage/migrations/v9-to-v10.ts b/apps/web/src/services/storage/migrations/v9-to-v10.ts index d61116f0..e156add9 100644 --- a/apps/web/src/services/storage/migrations/v9-to-v10.ts +++ b/apps/web/src/services/storage/migrations/v9-to-v10.ts @@ -1,16 +1,14 @@ -import { StorageMigration } from "./base"; -import type { ProjectRecord } from "./transformers/types"; +import { StorageMigration, type StorageMigrationRunArgs } from "./base"; +import type { MigrationResult, ProjectRecord } from "./transformers/types"; import { transformProjectV9ToV10 } from "./transformers/v9-to-v10"; export class V9toV10Migration extends StorageMigration { from = 9; to = 10; - async transform(project: ProjectRecord): Promise<{ - project: ProjectRecord; - skipped: boolean; - reason?: string; - }> { + async run({ + project, + }: StorageMigrationRunArgs): Promise> { return transformProjectV9ToV10({ project }); } }