summaryrefslogtreecommitdiff
path: root/apps/web/src/services/storage/migrations
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/src/services/storage/migrations')
-rw-r--r--apps/web/src/services/storage/migrations/__tests__/v28-to-v29.test.ts125
-rw-r--r--apps/web/src/services/storage/migrations/index.ts4
-rw-r--r--apps/web/src/services/storage/migrations/transformers/index.ts1
-rw-r--r--apps/web/src/services/storage/migrations/transformers/v28-to-v29.ts243
-rw-r--r--apps/web/src/services/storage/migrations/v28-to-v29.ts14
5 files changed, 386 insertions, 1 deletions
diff --git a/apps/web/src/services/storage/migrations/__tests__/v28-to-v29.test.ts b/apps/web/src/services/storage/migrations/__tests__/v28-to-v29.test.ts
new file mode 100644
index 00000000..dc76b6fd
--- /dev/null
+++ b/apps/web/src/services/storage/migrations/__tests__/v28-to-v29.test.ts
@@ -0,0 +1,125 @@
+import { describe, expect, test } from "bun:test";
+import { transformProjectV28ToV29 } from "../transformers/v28-to-v29";
+import { asRecord, asRecordArray } from "./helpers";
+
+describe("V28 to V29 Migration", () => {
+ test("moves built-in element fields into params", () => {
+ const result = transformProjectV28ToV29({
+ project: {
+ id: "project-v28-builtins",
+ version: 28,
+ scenes: [
+ {
+ id: "scene-1",
+ tracks: {
+ main: {
+ id: "main",
+ type: "video",
+ elements: [
+ {
+ id: "video-1",
+ type: "video",
+ name: "Video",
+ transform: {
+ position: { x: 12, y: -4 },
+ scaleX: 1.25,
+ scaleY: 0.75,
+ rotate: 9,
+ },
+ opacity: 0.5,
+ blendMode: "multiply",
+ volume: -6,
+ muted: true,
+ },
+ ],
+ },
+ overlay: [
+ {
+ id: "text-track",
+ type: "text",
+ elements: [
+ {
+ id: "text-1",
+ type: "text",
+ name: "Text",
+ content: "Hello",
+ fontSize: 20,
+ fontFamily: "Inter",
+ color: "#eeeeee",
+ textAlign: "left",
+ fontWeight: "bold",
+ fontStyle: "italic",
+ textDecoration: "underline",
+ letterSpacing: 1.5,
+ lineHeight: 1.4,
+ background: {
+ enabled: true,
+ color: "#111111",
+ paddingX: 10,
+ paddingY: 12,
+ },
+ transform: {
+ position: { x: 1, y: 2 },
+ scaleX: 1,
+ scaleY: 1,
+ rotate: 0,
+ },
+ opacity: 1,
+ },
+ ],
+ },
+ ],
+ audio: [],
+ },
+ },
+ ],
+ },
+ });
+
+ expect(result.skipped).toBe(false);
+ expect(result.project.version).toBe(29);
+
+ const scene = asRecordArray(result.project.scenes)[0];
+ const tracks = asRecord(scene.tracks);
+ const main = asRecord(tracks.main);
+ const video = asRecordArray(main.elements)[0];
+ expect(video.transform).toBeUndefined();
+ expect(video.opacity).toBeUndefined();
+ expect(video.volume).toBeUndefined();
+ expect(video.muted).toBeUndefined();
+ expect(video.params).toEqual({
+ "transform.positionX": 12,
+ "transform.positionY": -4,
+ "transform.scaleX": 1.25,
+ "transform.scaleY": 0.75,
+ "transform.rotate": 9,
+ opacity: 0.5,
+ blendMode: "multiply",
+ volume: -6,
+ muted: true,
+ });
+
+ const overlay = asRecordArray(tracks.overlay)[0];
+ const text = asRecordArray(asRecord(overlay).elements)[0];
+ expect(text.content).toBeUndefined();
+ expect(text.background).toBeUndefined();
+ expect(text.params).toMatchObject({
+ content: "Hello",
+ fontSize: 20,
+ fontFamily: "Inter",
+ color: "#eeeeee",
+ textAlign: "left",
+ fontWeight: "bold",
+ fontStyle: "italic",
+ textDecoration: "underline",
+ letterSpacing: 1.5,
+ lineHeight: 1.4,
+ "background.enabled": true,
+ "background.color": "#111111",
+ "background.paddingX": 10,
+ "background.paddingY": 12,
+ "transform.positionX": 1,
+ "transform.positionY": 2,
+ });
+ });
+});
diff --git a/apps/web/src/services/storage/migrations/index.ts b/apps/web/src/services/storage/migrations/index.ts
index 150da7c4..2e2c83bc 100644
--- a/apps/web/src/services/storage/migrations/index.ts
+++ b/apps/web/src/services/storage/migrations/index.ts
@@ -27,10 +27,11 @@ import { V24toV25Migration } from "./v24-to-v25";
import { V25toV26Migration } from "./v25-to-v26";
import { V26toV27Migration } from "./v26-to-v27";
import { V27toV28Migration } from "./v27-to-v28";
+import { V28toV29Migration } from "./v28-to-v29";
export { runStorageMigrations } from "./runner";
export type { MigrationProgress } from "./runner";
-export const CURRENT_PROJECT_VERSION = 28;
+export const CURRENT_PROJECT_VERSION = 29;
export const migrations = [
new V0toV1Migration(),
@@ -61,4 +62,5 @@ export const migrations = [
new V25toV26Migration(),
new V26toV27Migration(),
new V27toV28Migration(),
+ new V28toV29Migration(),
];
diff --git a/apps/web/src/services/storage/migrations/transformers/index.ts b/apps/web/src/services/storage/migrations/transformers/index.ts
index c1ae894f..ace19354 100644
--- a/apps/web/src/services/storage/migrations/transformers/index.ts
+++ b/apps/web/src/services/storage/migrations/transformers/index.ts
@@ -4,4 +4,5 @@ export { transformProjectV2ToV3 } from "./v2-to-v3";
export { transformProjectV3ToV4 } from "./v3-to-v4";
export { transformProjectV4ToV5 } from "./v4-to-v5";
export { transformProjectV17ToV18 } from "./v17-to-v18";
+export { transformProjectV28ToV29 } from "./v28-to-v29";
export type { MigrationResult, ProjectRecord } from "./types";
diff --git a/apps/web/src/services/storage/migrations/transformers/v28-to-v29.ts b/apps/web/src/services/storage/migrations/transformers/v28-to-v29.ts
new file mode 100644
index 00000000..c7f68a34
--- /dev/null
+++ b/apps/web/src/services/storage/migrations/transformers/v28-to-v29.ts
@@ -0,0 +1,243 @@
+import type { MigrationResult, ProjectRecord } from "./types";
+import { getProjectId, isRecord } from "./utils";
+
+export function transformProjectV28ToV29({
+ project,
+}: {
+ project: ProjectRecord;
+}): MigrationResult<ProjectRecord> {
+ if (!getProjectId({ project })) {
+ return { project, skipped: true, reason: "no project id" };
+ }
+
+ const version = project.version;
+ if (typeof version !== "number") {
+ return { project, skipped: true, reason: "invalid version" };
+ }
+ if (version >= 29) {
+ return { project, skipped: true, reason: "already v29" };
+ }
+ if (version !== 28) {
+ return { project, skipped: true, reason: "not v28" };
+ }
+
+ return {
+ project: {
+ ...migrateProject({ project }),
+ version: 29,
+ },
+ skipped: false,
+ };
+}
+
+function migrateProject({ project }: { project: ProjectRecord }): ProjectRecord {
+ const nextProject = { ...project };
+ if (Array.isArray(project.scenes)) {
+ nextProject.scenes = project.scenes.map((scene) => migrateScene({ scene }));
+ }
+ return nextProject;
+}
+
+function migrateScene({ scene }: { scene: unknown }): unknown {
+ if (!isRecord(scene)) {
+ return scene;
+ }
+
+ const nextScene = { ...scene };
+ if (isRecord(scene.tracks)) {
+ nextScene.tracks = migrateTracks({ tracks: scene.tracks });
+ }
+ return nextScene;
+}
+
+function migrateTracks({ tracks }: { tracks: ProjectRecord }): ProjectRecord {
+ const nextTracks = { ...tracks };
+ if (isRecord(tracks.main)) {
+ nextTracks.main = migrateTrack({ track: tracks.main });
+ }
+ if (Array.isArray(tracks.overlay)) {
+ nextTracks.overlay = tracks.overlay.map((track) => migrateTrack({ track }));
+ }
+ if (Array.isArray(tracks.audio)) {
+ nextTracks.audio = tracks.audio.map((track) => migrateTrack({ track }));
+ }
+ return nextTracks;
+}
+
+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)) {
+ return element;
+ }
+
+ const nextElement = { ...element };
+ const params: ProjectRecord = isRecord(element.params)
+ ? { ...element.params }
+ : {};
+
+ copyTransformParams({ source: element, params });
+ copyPrimitiveParam({ source: element, params, sourceKey: "opacity", paramKey: "opacity" });
+ copyPrimitiveParam({
+ source: element,
+ params,
+ sourceKey: "blendMode",
+ paramKey: "blendMode",
+ });
+
+ if (element.type === "audio" || element.type === "video") {
+ copyPrimitiveParam({
+ source: element,
+ params,
+ sourceKey: "volume",
+ paramKey: "volume",
+ });
+ copyPrimitiveParam({
+ source: element,
+ params,
+ sourceKey: "muted",
+ paramKey: "muted",
+ });
+ }
+
+ if (element.type === "text") {
+ copyTextParams({ source: element, params });
+ }
+
+ nextElement.params = params;
+ delete nextElement.transform;
+ delete nextElement.opacity;
+ delete nextElement.blendMode;
+ delete nextElement.volume;
+ delete nextElement.muted;
+ delete nextElement.content;
+ delete nextElement.fontSize;
+ delete nextElement.fontFamily;
+ delete nextElement.color;
+ delete nextElement.background;
+ delete nextElement.textAlign;
+ delete nextElement.fontWeight;
+ delete nextElement.fontStyle;
+ delete nextElement.textDecoration;
+ delete nextElement.letterSpacing;
+ delete nextElement.lineHeight;
+
+ return nextElement;
+}
+
+function copyTransformParams({
+ source,
+ params,
+}: {
+ source: ProjectRecord;
+ params: ProjectRecord;
+}): void {
+ if (!isRecord(source.transform)) {
+ return;
+ }
+ if (isRecord(source.transform.position)) {
+ copyPrimitiveParam({
+ source: source.transform.position,
+ params,
+ sourceKey: "x",
+ paramKey: "transform.positionX",
+ });
+ copyPrimitiveParam({
+ source: source.transform.position,
+ params,
+ sourceKey: "y",
+ paramKey: "transform.positionY",
+ });
+ }
+ copyPrimitiveParam({
+ source: source.transform,
+ params,
+ sourceKey: "scaleX",
+ paramKey: "transform.scaleX",
+ });
+ copyPrimitiveParam({
+ source: source.transform,
+ params,
+ sourceKey: "scaleY",
+ paramKey: "transform.scaleY",
+ });
+ copyPrimitiveParam({
+ source: source.transform,
+ params,
+ sourceKey: "rotate",
+ paramKey: "transform.rotate",
+ });
+}
+
+function copyTextParams({
+ source,
+ params,
+}: {
+ source: ProjectRecord;
+ params: ProjectRecord;
+}): void {
+ for (const key of [
+ "content",
+ "fontSize",
+ "fontFamily",
+ "color",
+ "textAlign",
+ "fontWeight",
+ "fontStyle",
+ "textDecoration",
+ "letterSpacing",
+ "lineHeight",
+ ]) {
+ copyPrimitiveParam({ source, params, sourceKey: key, paramKey: key });
+ }
+
+ if (!isRecord(source.background)) {
+ return;
+ }
+ for (const key of [
+ "enabled",
+ "color",
+ "cornerRadius",
+ "paddingX",
+ "paddingY",
+ "offsetX",
+ "offsetY",
+ ]) {
+ copyPrimitiveParam({
+ source: source.background,
+ params,
+ sourceKey: key,
+ paramKey: `background.${key}`,
+ });
+ }
+}
+
+function copyPrimitiveParam({
+ source,
+ params,
+ sourceKey,
+ paramKey,
+}: {
+ source: ProjectRecord;
+ params: ProjectRecord;
+ sourceKey: string;
+ paramKey: string;
+}): void {
+ const value = source[sourceKey];
+ if (
+ typeof value === "number" ||
+ typeof value === "string" ||
+ typeof value === "boolean"
+ ) {
+ params[paramKey] = value;
+ }
+}
diff --git a/apps/web/src/services/storage/migrations/v28-to-v29.ts b/apps/web/src/services/storage/migrations/v28-to-v29.ts
new file mode 100644
index 00000000..a2e75ec0
--- /dev/null
+++ b/apps/web/src/services/storage/migrations/v28-to-v29.ts
@@ -0,0 +1,14 @@
+import { StorageMigration, type StorageMigrationRunArgs } from "./base";
+import type { MigrationResult, ProjectRecord } from "./transformers/types";
+import { transformProjectV28ToV29 } from "./transformers/v28-to-v29";
+
+export class V28toV29Migration extends StorageMigration {
+ from = 28;
+ to = 29;
+
+ async run({
+ project,
+ }: StorageMigrationRunArgs): Promise<MigrationResult<ProjectRecord>> {
+ return transformProjectV28ToV29({ project });
+ }
+}