diff --git a/apps/web/src/services/storage/migrations/index.ts b/apps/web/src/services/storage/migrations/index.ts index bb2d9709..c5a2e237 100644 --- a/apps/web/src/services/storage/migrations/index.ts +++ b/apps/web/src/services/storage/migrations/index.ts @@ -24,10 +24,11 @@ import { V21toV22Migration } from "./v21-to-v22"; import { V22toV23Migration } from "./v22-to-v23"; import { V23toV24Migration } from "./v23-to-v24"; import { V24toV25Migration } from "./v24-to-v25"; +import { V25toV26Migration } from "./v25-to-v26"; export { runStorageMigrations } from "./runner"; export type { MigrationProgress } from "./runner"; -export const CURRENT_PROJECT_VERSION = 25; +export const CURRENT_PROJECT_VERSION = 26; export const migrations = [ new V0toV1Migration(), @@ -55,4 +56,5 @@ export const migrations = [ new V22toV23Migration(), new V23toV24Migration(), new V24toV25Migration(), + new V25toV26Migration(), ]; diff --git a/apps/web/src/services/storage/migrations/transformers/v23-to-v24.ts b/apps/web/src/services/storage/migrations/transformers/v23-to-v24.ts index 710d4dc1..736ca8dd 100644 --- a/apps/web/src/services/storage/migrations/transformers/v23-to-v24.ts +++ b/apps/web/src/services/storage/migrations/transformers/v23-to-v24.ts @@ -51,9 +51,7 @@ function migrateScene({ scene }: { scene: unknown }): unknown { } const mainTrack = findMainTrack({ tracks: scene.tracks }); - if (!mainTrack) { - return scene; - } + const finalMainTrack = mainTrack ?? buildEmptyMainTrack(); return { ...scene, @@ -65,7 +63,7 @@ function migrateScene({ scene }: { scene: unknown }): unknown { (track): track is ProjectRecord => isRecord(track) && track.type !== "audio", ), - main: migrateTrack({ track: mainTrack }), + main: migrateTrack({ track: finalMainTrack }), audio: scene.tracks .map((track) => migrateTrack({ track })) .filter( @@ -76,6 +74,17 @@ function migrateScene({ scene }: { scene: unknown }): unknown { }; } +function buildEmptyMainTrack(): ProjectRecord { + return { + id: crypto.randomUUID(), + name: "Main", + type: "video", + elements: [], + muted: false, + hidden: false, + }; +} + function findMainTrack({ tracks, }: { diff --git a/apps/web/src/services/storage/migrations/transformers/v25-to-v26.ts b/apps/web/src/services/storage/migrations/transformers/v25-to-v26.ts new file mode 100644 index 00000000..f00383e6 --- /dev/null +++ b/apps/web/src/services/storage/migrations/transformers/v25-to-v26.ts @@ -0,0 +1,129 @@ +import type { MigrationResult, ProjectRecord } from "./types"; +import { getProjectId, isRecord } from "./utils"; + +export function transformProjectV25ToV26({ + project, +}: { + project: ProjectRecord; +}): MigrationResult { + if (!getProjectId({ project })) { + return { project, skipped: true, reason: "no project id" }; + } + + const version = project.version; + if (typeof version !== "number") { + return { project, skipped: true, reason: "invalid version" }; + } + if (version >= 26) { + return { project, skipped: true, reason: "already v26" }; + } + if (version !== 25) { + return { project, skipped: true, reason: "not v25" }; + } + + return { + project: { + ...migrateProject({ project }), + version: 26, + }, + skipped: false, + }; +} + +function migrateProject({ + project, +}: { + project: ProjectRecord; +}): ProjectRecord { + if (!Array.isArray(project.scenes)) { + return project; + } + + return { + ...project, + scenes: project.scenes.map((scene) => migrateScene({ scene })), + }; +} + +function migrateScene({ scene }: { scene: unknown }): unknown { + if (!isRecord(scene)) { + return scene; + } + + const tracks = scene.tracks; + + // Already in the correct shape — nothing to repair + if (isRecord(tracks) && Array.isArray(tracks.overlay) && Array.isArray(tracks.audio)) { + return scene; + } + + // Reconstruct the flat track array from whatever broken state it's in. + // v24-to-v25 spread a flat array into a numeric-keyed object, so + // Object.values recovers the original elements. A true flat array is + // also handled for safety. + let trackArray: unknown[]; + if (Array.isArray(tracks)) { + trackArray = tracks; + } else if (isRecord(tracks)) { + trackArray = Object.values(tracks); + } else { + trackArray = []; + } + + const mainTrack = findMainTrack({ tracks: trackArray }); + const finalMainTrack = mainTrack ?? buildEmptyMainTrack(); + + return { + ...scene, + tracks: { + main: migrateTrack({ track: finalMainTrack }), + overlay: trackArray + .filter((track) => track !== mainTrack) + .map((track) => migrateTrack({ track })) + .filter( + (track): track is ProjectRecord => + isRecord(track) && track.type !== "audio", + ), + audio: trackArray + .map((track) => migrateTrack({ track })) + .filter( + (track): track is ProjectRecord => + isRecord(track) && track.type === "audio", + ), + }, + }; +} + +function findMainTrack({ + tracks, +}: { + tracks: unknown[]; +}): ProjectRecord | null { + for (const track of tracks) { + if (!isRecord(track)) continue; + if (track.type === "video" && track.isMain === true) return track; + } + for (const track of tracks) { + if (!isRecord(track)) continue; + if (track.type === "video") return track; + } + return null; +} + +function migrateTrack({ track }: { track: unknown }): unknown { + if (!isRecord(track)) return track; + const nextTrack = { ...track }; + delete nextTrack.isMain; + return nextTrack; +} + +function buildEmptyMainTrack(): ProjectRecord { + return { + id: crypto.randomUUID(), + name: "Main", + type: "video", + elements: [], + muted: false, + hidden: false, + }; +} diff --git a/apps/web/src/services/storage/migrations/v25-to-v26.ts b/apps/web/src/services/storage/migrations/v25-to-v26.ts new file mode 100644 index 00000000..b5a8f310 --- /dev/null +++ b/apps/web/src/services/storage/migrations/v25-to-v26.ts @@ -0,0 +1,16 @@ +import { StorageMigration } from "./base"; +import type { 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; + }> { + return transformProjectV25ToV26({ project }); + } +}