From 4c921b4c4a554f90a2e93a692fc813f023c51210 Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Sat, 24 Oct 2026 13:52:41 +0100 Subject: [PATCH 1/3] fix: keep pending Loom conversions pointed at the owner's original --- .../unit/loom-import-backfill.test.ts | 40 +++++++++++++++++++ .../unit/loom-operator-tools.test.ts | 39 +++++++++++++++++- apps/web/lib/loom-import-backfill.ts | 27 +++++++++++++ apps/web/lib/loom-import-reassign.ts | 4 ++ 4 files changed, 108 insertions(+), 2 deletions(-) diff --git a/apps/web/__tests__/unit/loom-import-backfill.test.ts b/apps/web/__tests__/unit/loom-import-backfill.test.ts index d8ec6f3aaf..ce34f61b96 100644 --- a/apps/web/__tests__/unit/loom-import-backfill.test.ts +++ b/apps/web/__tests__/unit/loom-import-backfill.test.ts @@ -301,6 +301,46 @@ describe("backfillLoomImport", () => { expect(mocks.reassign).not.toHaveBeenCalled(); }); + it("repairs a pending conversion left on the previous owner's path", async () => { + mocks.selects = [ + [ + video({ + ownerId: "zephyaaaaaaaaaa", + metadata: { + loomDeferredProcessing: { + status: "pending", + rawFileKey: "lukeaaaaaaaaaaa/videoaaaaaaaaaa/raw-upload.webm", + }, + }, + }), + ], + [lukeFolder("folderaaaaaaaaa")], + ]; + await expect( + backfillLoomImport(org, { videoId: "videoaaaaaaaaaa" }), + ).resolves.toMatchObject({ status: "updated", changes: ["original"] }); + expect(mocks.updates).toHaveLength(1); + }); + + it("leaves a conversion already on the owner's path alone", async () => { + mocks.selects = [ + [ + video({ + metadata: { + loomDeferredProcessing: { + status: "pending", + rawFileKey: "lukeaaaaaaaaaaa/videoaaaaaaaaaa/raw-upload.webm", + }, + }, + }), + ], + [lukeFolder("folderaaaaaaaaa")], + ]; + await expect( + backfillLoomImport(org, { videoId: "videoaaaaaaaaaa" }), + ).resolves.toMatchObject({ status: "unchanged" }); + }); + it("reports moves it could not make or finish cleaning up", async () => { mocks.selects = [[video()], [lukeFolder("folderaaaaaaaaa")]]; mocks.reassign.mockResolvedValue({ diff --git a/apps/web/__tests__/unit/loom-operator-tools.test.ts b/apps/web/__tests__/unit/loom-operator-tools.test.ts index 98738ee1b6..f1eead7014 100644 --- a/apps/web/__tests__/unit/loom-operator-tools.test.ts +++ b/apps/web/__tests__/unit/loom-operator-tools.test.ts @@ -1,4 +1,6 @@ import type { HttpApi } from "@effect/platform"; +import type { SQL } from "drizzle-orm"; +import { MySqlDialect } from "drizzle-orm/mysql-core"; import { Effect, type Layer as EffectLayer, Option } from "effect"; import { NextRequest } from "next/server"; import { beforeEach, describe, expect, it, vi } from "vitest"; @@ -293,7 +295,11 @@ describe("reassignLoomImport", () => { ], ]); expect(mocks.updates).toEqual([ - { ownerId: ids.to, public: false, folderId: null }, + expect.objectContaining({ + ownerId: ids.to, + public: false, + folderId: null, + }), ]); expect(mocks.deletes).toBe(2); expect(mocks.removed).toEqual([mocks.objects.map(({ Key }) => Key)]); @@ -310,6 +316,19 @@ describe("reassignLoomImport", () => { [importedVideo({ source: { type: "webMP4", outputKey: "edited.mp4" } })], "edited video", ], + [ + [ + importedVideo({ + metadata: { + loomDeferredProcessing: { + status: "started", + rawFileKey: `${ids.from}/${ids.video}/raw-upload.webm`, + }, + }, + }), + ], + "conversion running", + ], [[importedVideo(), []], "new owner not a member"], ])("leaves unsafe moves alone: %#", async (selects, reason) => { mocks.selects = (selects as unknown[]).map((row) => @@ -341,6 +360,21 @@ describe("reassignLoomImport", () => { expect(mocks.claims).toHaveLength(2); }); + it("points a pending conversion at the new owner's copy", async () => { + mocks.selects = [[importedVideo()], [{ userId: ids.to }]]; + await reassignLoomImport(input); + const query = new MySqlDialect().sqlToQuery( + mocks.updates[0]?.metadata as SQL, + ); + expect(query.sql).toContain("'$.loomDeferredProcessing.rawFileKey'"); + expect(query.params).toEqual( + expect.arrayContaining([ + `${ids.from}/${ids.video}/%`, + `${ids.to}/${ids.video}/`, + ]), + ); + }); + it("keeps sharing and visibility when asked", async () => { mocks.selects = [[importedVideo()], [{ userId: ids.to }]]; await expect( @@ -350,8 +384,9 @@ describe("reassignLoomImport", () => { }), ).resolves.toMatchObject({ status: "moved" }); expect(mocks.updates).toEqual([ - { ownerId: ids.to, folderId: "folderaaaaaaaaa" }, + expect.objectContaining({ ownerId: ids.to, folderId: "folderaaaaaaaaa" }), ]); + expect(mocks.updates[0]).not.toHaveProperty("public"); expect(mocks.deletes).toBe(0); }); diff --git a/apps/web/lib/loom-import-backfill.ts b/apps/web/lib/loom-import-backfill.ts index f7bee4584a..5df3578637 100644 --- a/apps/web/lib/loom-import-backfill.ts +++ b/apps/web/lib/loom-import-backfill.ts @@ -58,6 +58,7 @@ export async function backfillLoomImport( ownerId: videos.ownerId, folderId: videos.folderId, effectiveCreatedAt: videos.effectiveCreatedAt, + metadata: videos.metadata, orgOwnerId: organizations.ownerId, }) .from(videos) @@ -153,6 +154,32 @@ export async function backfillLoomImport( changes.push("folder"); } + const deferredKey = video.metadata?.loomDeferredProcessing?.rawFileKey; + const ownerPrefix = `${ownerId}/${item.videoId}/`; + if ( + ownerId === video.ownerId && + video.metadata?.loomDeferredProcessing?.status === "pending" && + typeof deferredKey === "string" && + !deferredKey.startsWith(ownerPrefix) + ) { + const fileName = deferredKey.split("/").slice(2).join("/"); + if (deferredKey.split("/")[1] === item.videoId && fileName) { + await db() + .update(videos) + .set({ + metadata: sql`JSON_SET(${videos.metadata}, '$.loomDeferredProcessing.rawFileKey', ${`${ownerPrefix}${fileName}`})`, + }) + .where( + and( + eq(videos.id, id), + eq(videos.ownerId, ownerId), + sql`JSON_UNQUOTE(JSON_EXTRACT(${videos.metadata}, '$.loomDeferredProcessing.rawFileKey')) = ${deferredKey}`, + ), + ); + changes.push("original"); + } + } + if (date && video.effectiveCreatedAt?.getTime() !== date.getTime()) { await db() .update(videos) diff --git a/apps/web/lib/loom-import-reassign.ts b/apps/web/lib/loom-import-reassign.ts index 4e5c9a7a29..f9c9cf82ba 100644 --- a/apps/web/lib/loom-import-reassign.ts +++ b/apps/web/lib/loom-import-reassign.ts @@ -104,6 +104,7 @@ export async function reassignLoomImport({ ownerId: videos.ownerId, orgId: videos.orgId, source: videos.source, + metadata: videos.metadata, bucketId: videos.bucket, storageIntegrationId: videos.storageIntegrationId, uploadPhase: videoUploads.phase, @@ -132,6 +133,8 @@ export async function reassignLoomImport({ return { videoId, status: "skipped", reason: "import not finished" }; if (video.source && "outputKey" in video.source && video.source.outputKey) return { videoId, status: "skipped", reason: "edited video" }; + if (video.metadata?.loomDeferredProcessing?.status === "started") + return { videoId, status: "skipped", reason: "conversion running" }; const [member] = await db() .select({ userId: organizationMembers.userId }) @@ -273,6 +276,7 @@ async function moveClaimedVideo({ const result = await tx .update(videos) .set({ + metadata: sql`IF(JSON_UNQUOTE(JSON_EXTRACT(${videos.metadata}, '$.loomDeferredProcessing.rawFileKey')) LIKE ${`${fromUserId}/${videoId}/%`}, JSON_SET(${videos.metadata}, '$.loomDeferredProcessing.rawFileKey', CONCAT(${`${toUserId}/${videoId}/`}, SUBSTRING(JSON_UNQUOTE(JSON_EXTRACT(${videos.metadata}, '$.loomDeferredProcessing.rawFileKey')), ${`${fromUserId}/${videoId}/`.length + 1}))), ${videos.metadata})`, ownerId: User.UserId.make(toUserId), folderId: options.folderId ? Folder.FolderId.make(options.folderId) From 24e0c0fb6e5645329ea09d3c5396adfa4e4cf272 Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Sat, 24 Oct 2026 13:57:47 +0100 Subject: [PATCH 2/3] fix: never move a Loom original while its conversion runs, repair any stale owner path --- .../unit/loom-import-backfill.test.ts | 11 +++++ .../unit/loom-operator-tools.test.ts | 43 ++++++++++++++++--- apps/web/lib/loom-deferred-processing.ts | 2 + apps/web/lib/loom-import-backfill.ts | 36 ++++++++-------- apps/web/lib/loom-import-reassign.ts | 25 ++++++++--- apps/web/lib/loom-original-key.ts | 13 ++++++ 6 files changed, 99 insertions(+), 31 deletions(-) create mode 100644 apps/web/lib/loom-original-key.ts diff --git a/apps/web/__tests__/unit/loom-import-backfill.test.ts b/apps/web/__tests__/unit/loom-import-backfill.test.ts index ce34f61b96..37e62a6ca1 100644 --- a/apps/web/__tests__/unit/loom-import-backfill.test.ts +++ b/apps/web/__tests__/unit/loom-import-backfill.test.ts @@ -1,3 +1,5 @@ +import type { SQL } from "drizzle-orm"; +import { MySqlDialect } from "drizzle-orm/mysql-core"; import { beforeEach, describe, expect, it, vi } from "vitest"; const mocks = vi.hoisted(() => ({ @@ -320,6 +322,15 @@ describe("backfillLoomImport", () => { backfillLoomImport(org, { videoId: "videoaaaaaaaaaa" }), ).resolves.toMatchObject({ status: "updated", changes: ["original"] }); expect(mocks.updates).toHaveLength(1); + const query = new MySqlDialect().sqlToQuery( + mocks.updates[0]?.metadata as SQL, + ); + expect(query.sql).toContain( + "JSON_SET(?, '$.loomDeferredProcessing.rawFileKey', ?)", + ); + expect(query.params).toContain( + "zephyaaaaaaaaaa/videoaaaaaaaaaa/raw-upload.webm", + ); }); it("leaves a conversion already on the owner's path alone", async () => { diff --git a/apps/web/__tests__/unit/loom-operator-tools.test.ts b/apps/web/__tests__/unit/loom-operator-tools.test.ts index f1eead7014..3f1d1c5c2c 100644 --- a/apps/web/__tests__/unit/loom-operator-tools.test.ts +++ b/apps/web/__tests__/unit/loom-operator-tools.test.ts @@ -156,6 +156,7 @@ import { encryptEditTranscriptObject, } from "@/lib/edit-transcript-storage"; import { reassignLoomImport } from "@/lib/loom-import-reassign"; +import { getOwnerOriginalKey } from "@/lib/loom-original-key"; const call = ( handler: (request: Request) => Promise, @@ -361,18 +362,34 @@ describe("reassignLoomImport", () => { }); it("points a pending conversion at the new owner's copy", async () => { - mocks.selects = [[importedVideo()], [{ userId: ids.to }]]; + mocks.selects = [ + [ + importedVideo({ + metadata: { + loomDeferredProcessing: { + status: "pending", + rawFileKey: `someoneaaaaaaaa/${ids.video}/raw-upload.webm`, + }, + }, + }), + ], + [{ userId: ids.to }], + ]; await reassignLoomImport(input); const query = new MySqlDialect().sqlToQuery( mocks.updates[0]?.metadata as SQL, ); - expect(query.sql).toContain("'$.loomDeferredProcessing.rawFileKey'"); - expect(query.params).toEqual( - expect.arrayContaining([ - `${ids.from}/${ids.video}/%`, - `${ids.to}/${ids.video}/`, - ]), + expect(query.sql).toContain("'$.loomDeferredProcessing.rawFileKey', ?)"); + expect(query.params).toContain( + `someoneaaaaaaaa/${ids.video}/raw-upload.webm`, ); + expect(query.params).toContain(`${ids.to}/${ids.video}/raw-upload.webm`); + }); + + it("leaves metadata alone for videos without a pending conversion", async () => { + mocks.selects = [[importedVideo()], [{ userId: ids.to }]]; + await reassignLoomImport(input); + expect(mocks.updates[0]).not.toHaveProperty("metadata"); }); it("keeps sharing and visibility when asked", async () => { @@ -493,3 +510,15 @@ describe("POST /api/internal/loom-imports/reassign", () => { }); }); }); + +describe("getOwnerOriginalKey", () => { + it.each([ + ["a/vid/raw-upload.webm", "b/vid/raw-upload.webm"], + ["a/vid/nested/file.webm", "b/vid/nested/file.webm"], + ["a/other/raw-upload.webm", null], + ["a/vid/", null], + [undefined, null], + ])("%s -> %s", (key, expected) => { + expect(getOwnerOriginalKey(key, "b", "vid")).toBe(expected); + }); +}); diff --git a/apps/web/lib/loom-deferred-processing.ts b/apps/web/lib/loom-deferred-processing.ts index 7a1ed19b7d..908aee4820 100644 --- a/apps/web/lib/loom-deferred-processing.ts +++ b/apps/web/lib/loom-deferred-processing.ts @@ -3,6 +3,7 @@ import { videos, videoUploads } from "@cap/database/schema"; import type { VideoMetadata } from "@cap/database/types"; import type { Video } from "@cap/web-domain"; import { and, eq, sql } from "drizzle-orm"; +import { LOOM_REASSIGN_CLAIM_TTL_MS } from "@/lib/loom-original-key"; import { startVideoProcessingWorkflow } from "@/lib/video-processing"; export const DEFERRED_PROCESSING_RECLAIM_MS = 30 * 60 * 1000; @@ -61,6 +62,7 @@ export async function startDeferredLoomProcessing( and( eq(videos.id, videoId), sql`JSON_UNQUOTE(JSON_EXTRACT(${videos.metadata}, '$.loomDeferredProcessing.rawFileKey')) = ${rawFileKey}`, + sql`(JSON_EXTRACT(${videos.metadata}, '$.loomReassignClaim') IS NULL OR JSON_UNQUOTE(JSON_EXTRACT(${videos.metadata}, '$.loomReassignClaim.at')) < ${new Date(now.getTime() - LOOM_REASSIGN_CLAIM_TTL_MS).toISOString()})`, sql`(JSON_UNQUOTE(JSON_EXTRACT(${videos.metadata}, '$.loomDeferredProcessing.status')) = 'pending' OR (JSON_UNQUOTE(JSON_EXTRACT(${videos.metadata}, '$.loomDeferredProcessing.status')) = 'started' AND JSON_UNQUOTE(JSON_EXTRACT(${videos.metadata}, '$.loomDeferredProcessing.startedAt')) < ${reclaimBefore}))`, ), ); diff --git a/apps/web/lib/loom-import-backfill.ts b/apps/web/lib/loom-import-backfill.ts index 5df3578637..e9d267f608 100644 --- a/apps/web/lib/loom-import-backfill.ts +++ b/apps/web/lib/loom-import-backfill.ts @@ -17,6 +17,7 @@ import { normalizeImportEmail, } from "@/lib/loom-csv-import"; import { reassignLoomImport } from "@/lib/loom-import-reassign"; +import { getOwnerOriginalKey } from "@/lib/loom-original-key"; import { provisionOrganizationInvitee } from "@/lib/organization-provisioning"; export type LoomBackfillItem = { @@ -155,29 +156,26 @@ export async function backfillLoomImport( } const deferredKey = video.metadata?.loomDeferredProcessing?.rawFileKey; - const ownerPrefix = `${ownerId}/${item.videoId}/`; + const repairedKey = getOwnerOriginalKey(deferredKey, ownerId, item.videoId); if ( ownerId === video.ownerId && video.metadata?.loomDeferredProcessing?.status === "pending" && - typeof deferredKey === "string" && - !deferredKey.startsWith(ownerPrefix) + repairedKey && + repairedKey !== deferredKey ) { - const fileName = deferredKey.split("/").slice(2).join("/"); - if (deferredKey.split("/")[1] === item.videoId && fileName) { - await db() - .update(videos) - .set({ - metadata: sql`JSON_SET(${videos.metadata}, '$.loomDeferredProcessing.rawFileKey', ${`${ownerPrefix}${fileName}`})`, - }) - .where( - and( - eq(videos.id, id), - eq(videos.ownerId, ownerId), - sql`JSON_UNQUOTE(JSON_EXTRACT(${videos.metadata}, '$.loomDeferredProcessing.rawFileKey')) = ${deferredKey}`, - ), - ); - changes.push("original"); - } + await db() + .update(videos) + .set({ + metadata: sql`JSON_SET(${videos.metadata}, '$.loomDeferredProcessing.rawFileKey', ${repairedKey})`, + }) + .where( + and( + eq(videos.id, id), + eq(videos.ownerId, ownerId), + sql`JSON_UNQUOTE(JSON_EXTRACT(${videos.metadata}, '$.loomDeferredProcessing.rawFileKey')) = ${deferredKey}`, + ), + ); + changes.push("original"); } if (date && video.effectiveCreatedAt?.getTime() !== date.getTime()) { diff --git a/apps/web/lib/loom-import-reassign.ts b/apps/web/lib/loom-import-reassign.ts index f9c9cf82ba..697e6b8f60 100644 --- a/apps/web/lib/loom-import-reassign.ts +++ b/apps/web/lib/loom-import-reassign.ts @@ -19,6 +19,10 @@ import { decryptEditTranscriptObject, encryptEditTranscriptObject, } from "@/lib/edit-transcript-storage"; +import { + getOwnerOriginalKey, + LOOM_REASSIGN_CLAIM_TTL_MS, +} from "@/lib/loom-original-key"; import { runPromise } from "@/lib/server"; export type LoomReassignOptions = { @@ -35,8 +39,6 @@ export type LoomReassignOutcome = } | { videoId: string; status: "skipped" | "failed"; reason: string }; -const CLAIM_TTL_MS = 60 * 60 * 1000; - function getAffectedRows(result: unknown) { const header = Array.isArray(result) ? result[0] : result; return (header as { affectedRows?: number } | undefined)?.affectedRows ?? 0; @@ -163,7 +165,7 @@ export async function reassignLoomImport({ and( eq(videos.id, id), eq(videos.ownerId, User.UserId.make(fromUserId)), - sql`(JSON_EXTRACT(${videos.metadata}, '$.loomReassignClaim') IS NULL OR JSON_UNQUOTE(JSON_EXTRACT(${videos.metadata}, '$.loomReassignClaim.at')) < ${new Date(claimedAt.getTime() - CLAIM_TTL_MS).toISOString()})`, + sql`(JSON_EXTRACT(${videos.metadata}, '$.loomReassignClaim') IS NULL OR JSON_UNQUOTE(JSON_EXTRACT(${videos.metadata}, '$.loomReassignClaim.at')) < ${new Date(claimedAt.getTime() - LOOM_REASSIGN_CLAIM_TTL_MS).toISOString()})`, ), ); if (getAffectedRows(claim) !== 1) @@ -177,6 +179,7 @@ export async function reassignLoomImport({ toUserId, token, options, + originalKey: video.metadata?.loomDeferredProcessing?.rawFileKey, }); } finally { await db() @@ -201,6 +204,7 @@ async function moveClaimedVideo({ toUserId, token, options, + originalKey, }: { id: Video.VideoId; videoId: string; @@ -209,6 +213,7 @@ async function moveClaimedVideo({ toUserId: string; token: string; options: LoomReassignOptions; + originalKey: unknown; }): Promise { const source = await listObjects(videoId, fromUserId, organizationId); if (source.objects.length === 0) @@ -272,11 +277,16 @@ async function moveClaimedVideo({ } const copiedObjects = written.length; + const movedOriginalKey = getOwnerOriginalKey(originalKey, toUserId, videoId); const updated = await db().transaction(async (tx) => { const result = await tx .update(videos) .set({ - metadata: sql`IF(JSON_UNQUOTE(JSON_EXTRACT(${videos.metadata}, '$.loomDeferredProcessing.rawFileKey')) LIKE ${`${fromUserId}/${videoId}/%`}, JSON_SET(${videos.metadata}, '$.loomDeferredProcessing.rawFileKey', CONCAT(${`${toUserId}/${videoId}/`}, SUBSTRING(JSON_UNQUOTE(JSON_EXTRACT(${videos.metadata}, '$.loomDeferredProcessing.rawFileKey')), ${`${fromUserId}/${videoId}/`.length + 1}))), ${videos.metadata})`, + ...(movedOriginalKey + ? { + metadata: sql`IF(JSON_UNQUOTE(JSON_EXTRACT(${videos.metadata}, '$.loomDeferredProcessing.rawFileKey')) = ${originalKey}, JSON_SET(${videos.metadata}, '$.loomDeferredProcessing.rawFileKey', ${movedOriginalKey}), ${videos.metadata})`, + } + : {}), ownerId: User.UserId.make(toUserId), folderId: options.folderId ? Folder.FolderId.make(options.folderId) @@ -288,6 +298,7 @@ async function moveClaimedVideo({ eq(videos.id, id), eq(videos.ownerId, User.UserId.make(fromUserId)), sql`JSON_UNQUOTE(JSON_EXTRACT(${videos.metadata}, '$.loomReassignClaim.token')) = ${token}`, + sql`COALESCE(JSON_UNQUOTE(JSON_EXTRACT(${videos.metadata}, '$.loomDeferredProcessing.status')), '') <> 'started'`, ), ); if (getAffectedRows(result) !== 1) return false; @@ -301,7 +312,11 @@ async function moveClaimedVideo({ await runPromise( source.bucket.deleteObjects(written.map((key) => ({ Key: key }))), ).catch(() => undefined); - return { videoId, status: "failed", reason: "move claim was lost" }; + return { + videoId, + status: "failed", + reason: "video changed during the move", + }; } try { diff --git a/apps/web/lib/loom-original-key.ts b/apps/web/lib/loom-original-key.ts new file mode 100644 index 0000000000..def6f2d4dc --- /dev/null +++ b/apps/web/lib/loom-original-key.ts @@ -0,0 +1,13 @@ +export const LOOM_REASSIGN_CLAIM_TTL_MS = 60 * 60 * 1000; + +export function getOwnerOriginalKey( + key: unknown, + ownerId: string, + videoId: string, +) { + if (typeof key !== "string") return null; + const [, keyVideoId, ...rest] = key.split("/"); + const fileName = rest.join("/"); + if (keyVideoId !== videoId || !fileName) return null; + return `${ownerId}/${videoId}/${fileName}`; +} From e7d560014495a47ed5e5989ad6554dec653a6cc1 Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Sat, 24 Oct 2026 14:01:22 +0100 Subject: [PATCH 3/3] fix: roll back a Loom move when its original path changed underneath it --- apps/web/__tests__/unit/loom-operator-tools.test.ts | 5 +---- apps/web/lib/loom-import-reassign.ts | 3 ++- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/apps/web/__tests__/unit/loom-operator-tools.test.ts b/apps/web/__tests__/unit/loom-operator-tools.test.ts index 3f1d1c5c2c..9acf167052 100644 --- a/apps/web/__tests__/unit/loom-operator-tools.test.ts +++ b/apps/web/__tests__/unit/loom-operator-tools.test.ts @@ -380,10 +380,7 @@ describe("reassignLoomImport", () => { mocks.updates[0]?.metadata as SQL, ); expect(query.sql).toContain("'$.loomDeferredProcessing.rawFileKey', ?)"); - expect(query.params).toContain( - `someoneaaaaaaaa/${ids.video}/raw-upload.webm`, - ); - expect(query.params).toContain(`${ids.to}/${ids.video}/raw-upload.webm`); + expect(query.params).toEqual([`${ids.to}/${ids.video}/raw-upload.webm`]); }); it("leaves metadata alone for videos without a pending conversion", async () => { diff --git a/apps/web/lib/loom-import-reassign.ts b/apps/web/lib/loom-import-reassign.ts index 697e6b8f60..5e91a08e35 100644 --- a/apps/web/lib/loom-import-reassign.ts +++ b/apps/web/lib/loom-import-reassign.ts @@ -284,7 +284,7 @@ async function moveClaimedVideo({ .set({ ...(movedOriginalKey ? { - metadata: sql`IF(JSON_UNQUOTE(JSON_EXTRACT(${videos.metadata}, '$.loomDeferredProcessing.rawFileKey')) = ${originalKey}, JSON_SET(${videos.metadata}, '$.loomDeferredProcessing.rawFileKey', ${movedOriginalKey}), ${videos.metadata})`, + metadata: sql`JSON_SET(${videos.metadata}, '$.loomDeferredProcessing.rawFileKey', ${movedOriginalKey})`, } : {}), ownerId: User.UserId.make(toUserId), @@ -299,6 +299,7 @@ async function moveClaimedVideo({ eq(videos.ownerId, User.UserId.make(fromUserId)), sql`JSON_UNQUOTE(JSON_EXTRACT(${videos.metadata}, '$.loomReassignClaim.token')) = ${token}`, sql`COALESCE(JSON_UNQUOTE(JSON_EXTRACT(${videos.metadata}, '$.loomDeferredProcessing.status')), '') <> 'started'`, + sql`JSON_UNQUOTE(JSON_EXTRACT(${videos.metadata}, '$.loomDeferredProcessing.rawFileKey')) <=> ${typeof originalKey === "string" ? originalKey : null}`, ), ); if (getAffectedRows(result) !== 1) return false;