fix: move blob migration server-side, restore lightweight list loading

The client-side migration was downloading 25-50MB of base64 data to the
browser before showing anything. getAllEntries also lost its lightweight flag.

Fix:
- New POST /api/storage/migrate-blobs endpoint runs entirely server-side
  (loads entries one-at-a-time from PostgreSQL, never sends heavy data to browser)
- Restore lightweight:true on getAllEntries (strips remaining base64 in API)
- Migration fires on mount (fire-and-forget) while list loads independently
- Remove client-side migrateEntryBlobs function
This commit is contained in:
AI Assistant
2026-02-28 00:03:26 +02:00
parent 578f6580a4
commit a0ec4aed3f
3 changed files with 147 additions and 58 deletions
+139
View File
@@ -0,0 +1,139 @@
import { NextResponse } from "next/server";
import { prisma } from "@/core/storage/prisma";
/**
* Server-side migration: move base64 attachment data from registry entries
* to a separate "registratura-blobs" namespace. This runs entirely on the
* server — heavy data never reaches the browser.
*
* POST /api/storage/migrate-blobs
*/
export async function POST() {
try {
// 1. Check migration flag
const flag = await prisma.keyValueStore.findUnique({
where: {
namespace_key: {
namespace: "registratura",
key: "__blobs_migrated__",
},
},
});
if (flag) {
return NextResponse.json({ migrated: 0, alreadyDone: true });
}
// 2. Get ONLY keys (no heavy value column) for registratura entries
const keyRows = await prisma.keyValueStore.findMany({
where: { namespace: "registratura", key: { startsWith: "entry:" } },
select: { key: true },
});
let migrated = 0;
// 3. Process entries ONE AT A TIME server-side
for (const { key } of keyRows) {
const row = await prisma.keyValueStore.findUnique({
where: { namespace_key: { namespace: "registratura", key } },
});
if (!row?.value) continue;
const entry = row.value as Record<string, unknown>;
const entryId = (entry.id as string) || key.replace("entry:", "");
const blobs: Record<string, unknown> = {};
let hasBlobs = false;
// Strip attachment data
const attachments =
(entry.attachments as Array<Record<string, unknown>>) ?? [];
const strippedAttachments = attachments.map((att) => {
const data = att.data as string | undefined;
if (
data &&
typeof data === "string" &&
data.length > 1024 &&
data !== "__stripped__"
) {
if (!blobs.attachments) blobs.attachments = {};
(blobs.attachments as Record<string, string>)[att.id as string] =
data;
hasBlobs = true;
return { ...att, data: "" };
}
return att;
});
// Strip closure attachment data
let strippedEntry: Record<string, unknown> = {
...entry,
attachments: strippedAttachments,
};
const closureInfo = entry.closureInfo as
| Record<string, unknown>
| undefined;
if (closureInfo?.attachment) {
const closureAtt = closureInfo.attachment as Record<string, unknown>;
const data = closureAtt.data as string | undefined;
if (
data &&
typeof data === "string" &&
data.length > 1024 &&
data !== "__stripped__"
) {
blobs.closureAttachment = data;
hasBlobs = true;
strippedEntry = {
...strippedEntry,
closureInfo: {
...closureInfo,
attachment: { ...closureAtt, data: "" },
},
};
}
}
if (hasBlobs) {
// Save stripped entry + blobs in parallel
await Promise.all([
prisma.keyValueStore.update({
where: { namespace_key: { namespace: "registratura", key } },
data: { value: strippedEntry as object },
}),
prisma.keyValueStore.upsert({
where: {
namespace_key: { namespace: "registratura-blobs", key: entryId },
},
update: { value: blobs as object },
create: {
namespace: "registratura-blobs",
key: entryId,
value: blobs as object,
},
}),
]);
migrated++;
}
}
// 4. Set migration flag
await prisma.keyValueStore.upsert({
where: {
namespace_key: {
namespace: "registratura",
key: "__blobs_migrated__",
},
},
update: { value: true },
create: {
namespace: "registratura",
key: "__blobs_migrated__",
value: true,
},
});
return NextResponse.json({ migrated, alreadyDone: false });
} catch (error) {
console.error("Blob migration error:", error);
return NextResponse.json({ error: "Migration failed" }, { status: 500 });
}
}