refactor: migration run() and pure v1-to-v2 transform

This commit is contained in:
Maze Winther 2026-04-22 02:41:18 +02:00
parent a709af0059
commit f5f31b43df
36 changed files with 1000 additions and 838 deletions

View File

@ -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<string, unknown>;
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<string, unknown>;
const background = settings.background as Record<string, unknown>;
@ -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<string, unknown>;
const background = settings.background as Record<string, unknown>;
@ -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<Record<string, unknown>>;
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<string, unknown>;
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<MediaAssetData | null> => {
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<Record<string, unknown>>;
const mainScene = scenes[0];
expect(Array.isArray(mainScene.tracks)).toBe(true);
const tracks = mainScene.tracks as Array<Record<string, unknown>>;
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<Record<string, unknown>>;
expect(scenes.length).toBe(1);
const tracks = scenes[0].tracks as Array<Record<string, unknown>>;
const elements = tracks[0].elements as Array<Record<string, unknown>>;
const textElement = elements[0];
expect(textElement.opacity).toBe(0.5);
expect(textElement.transform).toEqual({
scale: 1,
position: { x: 120, y: 240 },
rotate: 15,
});
});
});

View File

@ -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<MigrationResult<ProjectRecord>>;
}
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<MigrationResult<ProjectRecord>>;
}

View File

@ -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<StorageMigrationResult> {
// 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<ProjectRecord>(
"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<StorageMigrationResult> {
// 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<ProjectRecord>(
"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;
}

View File

@ -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<string, unknown[]>;
mediaTypesById: Record<string, LegacyMediaType>;
}
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<MediaAssetData | null>;
}
export async function transformProjectV1ToV2({
export function transformProjectV1ToV2({
project,
options = {},
context = EMPTY_V1_TO_V2_CONTEXT,
}: {
project: ProjectRecord;
options?: TransformV1ToV2Options;
}): Promise<MigrationResult<ProjectRecord>> {
context?: V1ToV2Context;
}): MigrationResult<ProjectRecord> {
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<MediaAssetData | null>;
}): Promise<ProjectRecord> {
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<unknown[]> {
if (typeof indexedDB === "undefined") {
return [];
}
const sceneDbName = `video-editor-timelines-${projectId}-${sceneId}`;
const projectDbName = `video-editor-timelines-${projectId}`;
const adapter = new IndexedDBAdapter<LegacyTimelineData>(
sceneDbName,
"timeline",
1,
);
let data = await adapter.get("timeline");
if (!data && isMain) {
const projectAdapter = new IndexedDBAdapter<LegacyTimelineData>(
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<MediaAssetData | null>;
}): Promise<V2TimelineTrack[]> {
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<MediaAssetData | null>;
context: V1ToV2Context;
isMain: boolean;
}): Promise<V2VideoTrack> {
}): 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,

View File

@ -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<ProjectRecord> {
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<ProjectRecord> {
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 };
}

View File

@ -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<ProjectRecord> {
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<ProjectRecord> {
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));
}

View File

@ -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({

View File

@ -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 {

View File

@ -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<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"
);
}
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,
}: {

View File

@ -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<MigrationResult<ProjectRecord>> {
return transformProjectV0ToV1({ project });
}
}

View File

@ -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<MigrationResult<ProjectRecord>> {
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<MediaAssetData>(
`video-editor-media-${projectId}`,
"media-metadata",
1,
);
return mediaMetadataAdapter.get(mediaId);
};
}
function cleanupLegacyTimelineDBs({
async function loadV1ToV2Context({
projectId,
project,
}: {
projectId: string;
project: ProjectRecord;
}): void {
}): Promise<V1ToV2Context> {
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<V1ToV2Context["legacyTracksBySceneId"]> {
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<unknown[]> {
const sceneDbName = `video-editor-timelines-${projectId}-${sceneId}`;
const projectDbName = `video-editor-timelines-${projectId}`;
const adapter = new IndexedDBAdapter<LegacyTimelineData>(
sceneDbName,
"timeline",
1,
);
let data = await adapter.get("timeline");
if (!data && isMain) {
const projectAdapter = new IndexedDBAdapter<LegacyTimelineData>(
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<V1ToV2Context["mediaTypesById"]> {
const mediaIds = collectLegacyMediaIds({ legacyTracksBySceneId });
if (mediaIds.length === 0) {
return {};
}
const mediaMetadataAdapter = new IndexedDBAdapter<MediaAssetData>(
`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<string>();
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<void> {
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}`];
}

View File

@ -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<MigrationResult<ProjectRecord>> {
return transformProjectV10ToV11({ project });
}
}

View File

@ -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<MigrationResult<ProjectRecord>> {
return transformProjectV11ToV12({ project });
}
}

View File

@ -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<MigrationResult<ProjectRecord>> {
return transformProjectV12ToV13({ project });
}
}

View File

@ -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<MigrationResult<ProjectRecord>> {
return transformProjectV13ToV14({ project });
}
}

View File

@ -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<MigrationResult<ProjectRecord>> {
return transformProjectV14ToV15({ project });
}
}

View File

@ -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<MigrationResult<ProjectRecord>> {
return transformProjectV15ToV16({ project });
}
}

View File

@ -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<MigrationResult<ProjectRecord>> {
return transformProjectV16ToV17({ project });
}
}

View File

@ -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<MigrationResult<ProjectRecord>> {
return transformProjectV17ToV18({ project });
}
}

View File

@ -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<MigrationResult<ProjectRecord>> {
return transformProjectV18ToV19({ project });
}
}

View File

@ -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<MigrationResult<ProjectRecord>> {
return transformProjectV19ToV20({ project });
}
}

View File

@ -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<MigrationResult<ProjectRecord>> {
return transformProjectV2ToV3({ project });
}
}

View File

@ -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<MigrationResult<ProjectRecord>> {
return transformProjectV20ToV21({ project });
}
}

View File

@ -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<MigrationResult<ProjectRecord>> {
return transformProjectV21ToV22({ project });
}
}

View File

@ -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<MigrationResult<ProjectRecord>> {
return transformProjectV22ToV23({ project });
}
}

View File

@ -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<MigrationResult<ProjectRecord>> {
return transformProjectV23ToV24({ project });
}
}

View File

@ -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<MigrationResult<ProjectRecord>> {
return transformProjectV24ToV25({ project });
}
}

View File

@ -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<MigrationResult<ProjectRecord>> {
return transformProjectV25ToV26({ project });
}
}

View File

@ -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<MigrationResult<ProjectRecord>> {
return transformProjectV26ToV27({ project });
}
}

View File

@ -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<MigrationResult<ProjectRecord>> {
return transformProjectV3ToV4({ project });
}
}

View File

@ -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<MigrationResult<ProjectRecord>> {
return transformProjectV4ToV5({ project });
}
}

View File

@ -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<MigrationResult<ProjectRecord>> {
return transformProjectV5ToV6({ project });
}
}

View File

@ -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<MigrationResult<ProjectRecord>> {
return transformProjectV6ToV7({ project });
}
}

View File

@ -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<MigrationResult<ProjectRecord>> {
return transformProjectV7ToV8({ project });
}
}

View File

@ -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<MigrationResult<ProjectRecord>> {
return transformProjectV8ToV9({ project });
}
}

View File

@ -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<MigrationResult<ProjectRecord>> {
return transformProjectV9ToV10({ project });
}
}