refactor: migration run() and pure v1-to-v2 transform
This commit is contained in:
parent
a709af0059
commit
f5f31b43df
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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>>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}: {
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}`];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue