summaryrefslogtreecommitdiff
path: root/apps/web/src/services/storage
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/src/services/storage')
-rw-r--r--apps/web/src/services/storage/migrations/__tests__/v19-to-v20.test.ts141
-rw-r--r--apps/web/src/services/storage/migrations/index.ts4
-rw-r--r--apps/web/src/services/storage/migrations/transformers/v19-to-v20.ts87
-rw-r--r--apps/web/src/services/storage/migrations/v19-to-v20.ts16
4 files changed, 247 insertions, 1 deletions
diff --git a/apps/web/src/services/storage/migrations/__tests__/v19-to-v20.test.ts b/apps/web/src/services/storage/migrations/__tests__/v19-to-v20.test.ts
new file mode 100644
index 00000000..0e0cfbbe
--- /dev/null
+++ b/apps/web/src/services/storage/migrations/__tests__/v19-to-v20.test.ts
@@ -0,0 +1,141 @@
+import { describe, expect, test } from "bun:test";
+import { transformProjectV19ToV20 } from "../transformers/v19-to-v20";
+
+describe("V19 to V20 Migration", () => {
+ test("backfills source audio enabled state on video elements", () => {
+ const result = transformProjectV19ToV20({
+ project: {
+ id: "project-v19-source-audio",
+ version: 19,
+ metadata: {
+ id: "project-v19-source-audio",
+ name: "Project",
+ createdAt: "2024-01-01T00:00:00.000Z",
+ updatedAt: "2024-01-01T00:00:00.000Z",
+ },
+ settings: {
+ fps: 30,
+ canvasSize: { width: 1920, height: 1080 },
+ background: { type: "color", color: "#000000" },
+ },
+ currentSceneId: "scene-main",
+ scenes: [
+ {
+ id: "scene-main",
+ name: "Main",
+ isMain: true,
+ bookmarks: [],
+ createdAt: "2024-01-01T00:00:00.000Z",
+ updatedAt: "2024-01-01T00:00:00.000Z",
+ tracks: [
+ {
+ id: "track-video",
+ type: "video",
+ name: "Video",
+ isMain: true,
+ muted: false,
+ hidden: false,
+ elements: [
+ {
+ id: "video-1",
+ type: "video",
+ name: "Clip",
+ mediaId: "media-1",
+ duration: 5,
+ startTime: 0,
+ trimStart: 0,
+ trimEnd: 0,
+ transform: {
+ position: { x: 0, y: 0 },
+ scale: { x: 1, y: 1 },
+ rotation: 0,
+ },
+ opacity: 1,
+ },
+ ],
+ },
+ {
+ id: "track-audio",
+ type: "audio",
+ name: "Audio",
+ muted: false,
+ elements: [
+ {
+ id: "audio-1",
+ type: "audio",
+ sourceType: "upload",
+ mediaId: "media-audio-1",
+ name: "Audio",
+ duration: 5,
+ startTime: 0,
+ trimStart: 0,
+ trimEnd: 0,
+ volume: 0,
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ });
+
+ expect(result.skipped).toBe(false);
+ expect(result.project.version).toBe(20);
+ expect(
+ ((result.project.scenes as Array<Record<string, unknown>>)[0]
+ .tracks as Array<Record<string, unknown>>)[0].elements,
+ ).toEqual([
+ expect.objectContaining({
+ id: "video-1",
+ isSourceAudioEnabled: true,
+ }),
+ ]);
+ expect(
+ (((result.project.scenes as Array<Record<string, unknown>>)[0]
+ .tracks as Array<Record<string, unknown>>)[1]
+ .elements as Array<Record<string, unknown>>)[0].isSourceAudioEnabled,
+ ).toBeUndefined();
+ });
+
+ test("preserves existing explicit source audio state", () => {
+ const result = transformProjectV19ToV20({
+ project: {
+ id: "project-v19-existing-state",
+ version: 19,
+ scenes: [
+ {
+ tracks: [
+ {
+ elements: [
+ {
+ type: "video",
+ isSourceAudioEnabled: false,
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ });
+
+ expect(
+ (((result.project.scenes as Array<Record<string, unknown>>)[0]
+ .tracks as Array<Record<string, unknown>>)[0]
+ .elements as Array<Record<string, unknown>>)[0].isSourceAudioEnabled,
+ ).toBe(false);
+ });
+
+ test("skips projects already on v20", () => {
+ const result = transformProjectV19ToV20({
+ project: {
+ id: "project-v20",
+ version: 20,
+ },
+ });
+
+ expect(result.skipped).toBe(true);
+ expect(result.reason).toBe("already v20");
+ });
+});
diff --git a/apps/web/src/services/storage/migrations/index.ts b/apps/web/src/services/storage/migrations/index.ts
index e87d1cb9..d278263b 100644
--- a/apps/web/src/services/storage/migrations/index.ts
+++ b/apps/web/src/services/storage/migrations/index.ts
@@ -18,10 +18,11 @@ import { V15toV16Migration } from "./v15-to-v16";
import { V16toV17Migration } from "./v16-to-v17";
import { V17toV18Migration } from "./v17-to-v18";
import { V18toV19Migration } from "./v18-to-v19";
+import { V19toV20Migration } from "./v19-to-v20";
export { runStorageMigrations } from "./runner";
export type { MigrationProgress } from "./runner";
-export const CURRENT_PROJECT_VERSION = 19;
+export const CURRENT_PROJECT_VERSION = 20;
export const migrations = [
new V0toV1Migration(),
@@ -43,4 +44,5 @@ export const migrations = [
new V16toV17Migration(),
new V17toV18Migration(),
new V18toV19Migration(),
+ new V19toV20Migration(),
];
diff --git a/apps/web/src/services/storage/migrations/transformers/v19-to-v20.ts b/apps/web/src/services/storage/migrations/transformers/v19-to-v20.ts
new file mode 100644
index 00000000..f72812a4
--- /dev/null
+++ b/apps/web/src/services/storage/migrations/transformers/v19-to-v20.ts
@@ -0,0 +1,87 @@
+import type { MigrationResult, ProjectRecord } from "./types";
+import { getProjectId, isRecord } from "./utils";
+
+export function transformProjectV19ToV20({
+ project,
+}: {
+ project: ProjectRecord;
+}): MigrationResult<ProjectRecord> {
+ if (!getProjectId({ project })) {
+ return { project, skipped: true, reason: "no project id" };
+ }
+
+ if (typeof project.version === "number" && project.version >= 20) {
+ return { project, skipped: true, reason: "already v20" };
+ }
+
+ return {
+ project: {
+ ...migrateVideoSourceAudioState({ project }),
+ version: 20,
+ },
+ skipped: false,
+ };
+}
+
+function migrateVideoSourceAudioState({
+ project,
+}: {
+ project: ProjectRecord;
+}): ProjectRecord {
+ if (!Array.isArray(project.scenes)) {
+ return project;
+ }
+
+ return {
+ ...project,
+ scenes: project.scenes.map((scene) => migrateScene({ scene })),
+ };
+}
+
+function migrateScene({
+ scene,
+}: {
+ scene: unknown;
+}): unknown {
+ if (!isRecord(scene) || !Array.isArray(scene.tracks)) {
+ return scene;
+ }
+
+ return {
+ ...scene,
+ tracks: scene.tracks.map((track) => migrateTrack({ track })),
+ };
+}
+
+function migrateTrack({
+ track,
+}: {
+ track: unknown;
+}): unknown {
+ if (!isRecord(track) || !Array.isArray(track.elements)) {
+ return track;
+ }
+
+ return {
+ ...track,
+ elements: track.elements.map((element) => migrateElement({ element })),
+ };
+}
+
+function migrateElement({
+ element,
+}: {
+ element: unknown;
+}): unknown {
+ if (!isRecord(element) || element.type !== "video") {
+ return element;
+ }
+
+ return {
+ ...element,
+ isSourceAudioEnabled:
+ typeof element.isSourceAudioEnabled === "boolean"
+ ? element.isSourceAudioEnabled
+ : true,
+ };
+}
diff --git a/apps/web/src/services/storage/migrations/v19-to-v20.ts b/apps/web/src/services/storage/migrations/v19-to-v20.ts
new file mode 100644
index 00000000..c6809067
--- /dev/null
+++ b/apps/web/src/services/storage/migrations/v19-to-v20.ts
@@ -0,0 +1,16 @@
+import { StorageMigration } from "./base";
+import type { 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;
+ }> {
+ return transformProjectV19ToV20({ project });
+ }
+}