You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

113 lines
4.4 KiB

import fs from "node:fs/promises";
import path from "node:path";
import { dbGlobal } from "drizzle-pkg/lib/db";
import { users } from "drizzle-pkg/lib/schema/auth";
import { userConfigs } from "drizzle-pkg/lib/schema/config";
import { mediaAssets, mediaRefs, postComments, posts, timelineEvents } from "drizzle-pkg/lib/schema/content";
import { and, eq, inArray, lte, or } from "drizzle-orm";
import { MEDIA_REF_OWNER_POST } from "../../constants/media-refs";
import type { ExportMaskPolicy } from "../../constants/export";
import { sanitizeUserForExport } from "../../utils/export-mask";
export type BuildExportDataResult = {
dataFiles: Array<{ file: string; rowCount: number }>;
totalRows: number;
};
async function writeDataFile(baseDir: string, fileName: string, payload: unknown): Promise<{ file: string; rowCount: number }> {
const file = `data/${fileName}`;
const fullPath = path.resolve(baseDir, fileName);
await fs.writeFile(fullPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
const rowCount = Array.isArray(payload) ? payload.length : payload ? 1 : 0;
return { file, rowCount };
}
export async function buildExportDataJson(params: {
userId: number;
maskPolicy: ExportMaskPolicy;
cutoffAt: Date;
outputDataDir: string;
}): Promise<BuildExportDataResult> {
await fs.mkdir(params.outputDataDir, { recursive: true });
const [userRow] = await dbGlobal.select().from(users).where(eq(users.id, params.userId)).limit(1);
if (!userRow) {
throw new Error(`user not found: ${params.userId}`);
}
const sanitizedUser = sanitizeUserForExport(userRow as unknown as Record<string, unknown>, params.maskPolicy);
const [postRows, timelineRows, mediaRows, configRows] = await Promise.all([
dbGlobal
.select()
.from(posts)
.where(and(eq(posts.userId, params.userId), lte(posts.createdAt, params.cutoffAt))),
dbGlobal
.select()
.from(timelineEvents)
.where(and(eq(timelineEvents.userId, params.userId), lte(timelineEvents.createdAt, params.cutoffAt))),
dbGlobal
.select()
.from(mediaAssets)
.where(and(eq(mediaAssets.userId, params.userId), lte(mediaAssets.createdAt, params.cutoffAt))),
dbGlobal
.select()
.from(userConfigs)
.where(and(eq(userConfigs.userId, params.userId), lte(userConfigs.updatedAt, params.cutoffAt))),
]);
const postIds = postRows.map((p) => p.id);
const commentCondition =
postIds.length === 0
? and(lte(postComments.createdAt, params.cutoffAt), eq(postComments.authorUserId, params.userId))
: and(
lte(postComments.createdAt, params.cutoffAt),
or(eq(postComments.authorUserId, params.userId), inArray(postComments.postId, postIds)),
);
const commentRowsRaw = await dbGlobal.select().from(postComments).where(commentCondition);
const mediaRefRows =
postIds.length === 0
? []
: await dbGlobal
.select()
.from(mediaRefs)
.where(and(eq(mediaRefs.ownerType, MEDIA_REF_OWNER_POST), inArray(mediaRefs.ownerId, postIds)));
const commentRows = commentRowsRaw.map((row) =>
params.maskPolicy === "masked"
? sanitizeUserForExport(row as unknown as Record<string, unknown>, params.maskPolicy)
: row,
);
const configRowsMasked = configRows.map((row) =>
params.maskPolicy === "masked"
? sanitizeUserForExport(row as unknown as Record<string, unknown>, params.maskPolicy)
: row,
);
const dataFiles = await Promise.all([
writeDataFile(params.outputDataDir, "user.json", sanitizedUser),
writeDataFile(params.outputDataDir, "posts.json", postRows),
writeDataFile(params.outputDataDir, "timeline.json", timelineRows),
writeDataFile(params.outputDataDir, "comments.json", commentRows),
writeDataFile(params.outputDataDir, "media-assets.json", mediaRows),
writeDataFile(params.outputDataDir, "media-refs.json", mediaRefRows),
writeDataFile(params.outputDataDir, "user-configs.json", configRowsMasked),
]);
const totalRows = dataFiles.reduce((sum, item) => sum + item.rowCount, 0);
return { dataFiles, totalRows };
}
export async function listExportUserMediaAssets(params: {
userId: number;
cutoffAt: Date;
}): Promise<Array<{ storageKey: string }>> {
const rows = await dbGlobal
.select({
storageKey: mediaAssets.storageKey,
})
.from(mediaAssets)
.where(and(eq(mediaAssets.userId, params.userId), lte(mediaAssets.createdAt, params.cutoffAt)));
return rows;
}