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
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;
|
|
}
|
|
|