mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-16 19:00:38 +09:00
fix(ui): polish issue detail timelines and attachments
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
36376968af
commit
bd6d07d0b4
25 changed files with 2020 additions and 82 deletions
179
packages/db/src/backup-lib.test.ts
Normal file
179
packages/db/src/backup-lib.test.ts
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import postgres from "postgres";
|
||||
import { createBufferedTextFileWriter, runDatabaseBackup, runDatabaseRestore } from "./backup-lib.js";
|
||||
import { ensurePostgresDatabase } from "./client.js";
|
||||
import {
|
||||
getEmbeddedPostgresTestSupport,
|
||||
startEmbeddedPostgresTestDatabase,
|
||||
} from "./test-embedded-postgres.js";
|
||||
|
||||
const cleanups: Array<() => Promise<void> | void> = [];
|
||||
const embeddedPostgresSupport = await getEmbeddedPostgresTestSupport();
|
||||
const describeEmbeddedPostgres = embeddedPostgresSupport.supported ? describe : describe.skip;
|
||||
|
||||
function createTempDir(prefix: string): string {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
||||
cleanups.push(() => {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
});
|
||||
return dir;
|
||||
}
|
||||
|
||||
async function createTempDatabase(): Promise<string> {
|
||||
const db = await startEmbeddedPostgresTestDatabase("paperclip-db-backup-");
|
||||
cleanups.push(db.cleanup);
|
||||
return db.connectionString;
|
||||
}
|
||||
|
||||
async function createSiblingDatabase(connectionString: string, databaseName: string): Promise<string> {
|
||||
const adminUrl = new URL(connectionString);
|
||||
adminUrl.pathname = "/postgres";
|
||||
await ensurePostgresDatabase(adminUrl.toString(), databaseName);
|
||||
const targetUrl = new URL(connectionString);
|
||||
targetUrl.pathname = `/${databaseName}`;
|
||||
return targetUrl.toString();
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
while (cleanups.length > 0) {
|
||||
const cleanup = cleanups.pop();
|
||||
await cleanup?.();
|
||||
}
|
||||
});
|
||||
|
||||
if (!embeddedPostgresSupport.supported) {
|
||||
console.warn(
|
||||
`Skipping embedded Postgres backup tests on this host: ${embeddedPostgresSupport.reason ?? "unsupported environment"}`,
|
||||
);
|
||||
}
|
||||
|
||||
describe("createBufferedTextFileWriter", () => {
|
||||
it("preserves line boundaries across buffered flushes", async () => {
|
||||
const tempDir = createTempDir("paperclip-buffered-writer-");
|
||||
const outputPath = path.join(tempDir, "backup.sql");
|
||||
const writer = createBufferedTextFileWriter(outputPath, 16);
|
||||
const lines = [
|
||||
"-- header",
|
||||
"BEGIN;",
|
||||
"",
|
||||
"INSERT INTO test VALUES (1);",
|
||||
"-- footer",
|
||||
];
|
||||
|
||||
for (const line of lines) {
|
||||
writer.emit(line);
|
||||
}
|
||||
|
||||
await writer.close();
|
||||
|
||||
expect(fs.readFileSync(outputPath, "utf8")).toBe(lines.join("\n"));
|
||||
});
|
||||
});
|
||||
|
||||
describeEmbeddedPostgres("runDatabaseBackup", () => {
|
||||
it(
|
||||
"backs up and restores large table payloads without materializing one giant string",
|
||||
async () => {
|
||||
const sourceConnectionString = await createTempDatabase();
|
||||
const restoreConnectionString = await createSiblingDatabase(
|
||||
sourceConnectionString,
|
||||
"paperclip_restore_target",
|
||||
);
|
||||
const backupDir = createTempDir("paperclip-db-backup-output-");
|
||||
const sourceSql = postgres(sourceConnectionString, { max: 1, onnotice: () => {} });
|
||||
const restoreSql = postgres(restoreConnectionString, { max: 1, onnotice: () => {} });
|
||||
|
||||
try {
|
||||
await sourceSql.unsafe(`
|
||||
CREATE TYPE "public"."backup_test_state" AS ENUM ('pending', 'done');
|
||||
`);
|
||||
await sourceSql.unsafe(`
|
||||
CREATE TABLE "public"."backup_test_records" (
|
||||
"id" serial PRIMARY KEY,
|
||||
"title" text NOT NULL,
|
||||
"payload" text NOT NULL,
|
||||
"state" "public"."backup_test_state" NOT NULL,
|
||||
"metadata" jsonb,
|
||||
"created_at" timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
`);
|
||||
|
||||
const payload = "x".repeat(8192);
|
||||
for (let index = 0; index < 160; index += 1) {
|
||||
const createdAt = new Date(Date.UTC(2026, 0, 1, 0, 0, index));
|
||||
await sourceSql`
|
||||
INSERT INTO "public"."backup_test_records" (
|
||||
"title",
|
||||
"payload",
|
||||
"state",
|
||||
"metadata",
|
||||
"created_at"
|
||||
)
|
||||
VALUES (
|
||||
${`row-${index}`},
|
||||
${payload},
|
||||
${index % 2 === 0 ? "pending" : "done"}::"public"."backup_test_state",
|
||||
${JSON.stringify({ index, even: index % 2 === 0 })}::jsonb,
|
||||
${createdAt}
|
||||
)
|
||||
`;
|
||||
}
|
||||
|
||||
const result = await runDatabaseBackup({
|
||||
connectionString: sourceConnectionString,
|
||||
backupDir,
|
||||
retentionDays: 7,
|
||||
filenamePrefix: "paperclip-test",
|
||||
});
|
||||
|
||||
expect(result.backupFile).toMatch(/paperclip-test-.*\.sql$/);
|
||||
expect(result.sizeBytes).toBeGreaterThan(1024 * 1024);
|
||||
expect(fs.existsSync(result.backupFile)).toBe(true);
|
||||
|
||||
await runDatabaseRestore({
|
||||
connectionString: restoreConnectionString,
|
||||
backupFile: result.backupFile,
|
||||
});
|
||||
|
||||
const counts = await restoreSql.unsafe<{ count: number }[]>(`
|
||||
SELECT count(*)::int AS count
|
||||
FROM "public"."backup_test_records"
|
||||
`);
|
||||
expect(counts[0]?.count).toBe(160);
|
||||
|
||||
const sampleRows = await restoreSql.unsafe<{
|
||||
title: string;
|
||||
payload: string;
|
||||
state: string;
|
||||
metadata: { index: number; even: boolean };
|
||||
}[]>(`
|
||||
SELECT "title", "payload", "state"::text AS "state", "metadata"
|
||||
FROM "public"."backup_test_records"
|
||||
WHERE "title" IN ('row-0', 'row-159')
|
||||
ORDER BY "title"
|
||||
`);
|
||||
expect(sampleRows).toEqual([
|
||||
{
|
||||
title: "row-0",
|
||||
payload,
|
||||
state: "pending",
|
||||
metadata: { index: 0, even: true },
|
||||
},
|
||||
{
|
||||
title: "row-159",
|
||||
payload,
|
||||
state: "done",
|
||||
metadata: { index: 159, even: false },
|
||||
},
|
||||
]);
|
||||
} finally {
|
||||
await sourceSql.end();
|
||||
await restoreSql.end();
|
||||
}
|
||||
},
|
||||
60_000,
|
||||
);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue