feat(catalog): queue furniture imports with history and matching file attachments
CI / check (push) Successful in 1m30s
CI / deploy (push) Successful in 1m26s
CI / e2e (push) Successful in 25s

This commit is contained in:
Simo committed 2026-09-05 17:02:26 +02:00
1 parent 775d14f861
commit 2619bec165
16 files changed
+1016 -232

No files matched your search

+58
View File
@@ -0,0 +1,58 @@
import { randomUUID } from "node:crypto";
import { promises as fs } from "node:fs";
import path from "node:path";
import type { ImportJob } from "@/lib/furni/import-job";
export const importRoot = () =>
path.join(process.cwd(), "storage", "furniture-imports");
export const validJobId = (value: unknown): value is string =>
typeof value === "string" &&
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(
value,
);
export class ImportJobStore {
constructor(readonly root = importRoot()) {}
async save(job: ImportJob) {
await fs.mkdir(this.root, { recursive: true });
const target = path.join(this.root, `${job.id}.json`),
tmp = `${target}.${randomUUID()}.tmp`;
job.updatedAt = new Date().toISOString();
await fs.writeFile(tmp, JSON.stringify(job));
await fs.rename(tmp, target);
}
async create(job: ImportJob) {
if (!validJobId(job.id)) throw Error("Invalid import ID");
await fs.mkdir(this.root, { recursive: true });
const tmp = path.join(this.root, `${randomUUID()}.tmp`);
await fs.writeFile(tmp, JSON.stringify(job));
try {
await fs.link(tmp, path.join(this.root, `${job.id}.json`));
return job;
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
const existing = await this.read(job.id);
if (existing.userId !== job.userId)
throw Error("Request ID already used");
return existing;
} finally {
await fs.unlink(tmp);
}
}
async read(id: string): Promise<ImportJob> {
if (!validJobId(id)) throw Error("Invalid import ID");
return JSON.parse(
await fs.readFile(path.join(this.root, `${id}.json`), "utf8"),
);
}
async list(): Promise<ImportJob[]> {
const files = await fs.readdir(this.root).catch((error) => {
if (error.code === "ENOENT") return [];
throw error;
});
const jobs = await Promise.all(
files
.filter((f) => f.endsWith(".json") && validJobId(f.slice(0, -5)))
.map((f) => this.read(f.slice(0, -5))),
);
return jobs.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
}
}