feat(cms): improve catalog, editorial recovery and operations
CI / check (push) Successful in 52s
CI / deploy (push) Successful in 2m10s
CI / publish-container (push) Failing after 1m18s

This commit is contained in:
Simo committed 2026-09-09 19:36:15 +02:00
1 parent 2fead134e6
commit c389c3893d
122 files changed
+8137 -703

No files matched your search

@@ -7,6 +7,7 @@ const mocks = vi.hoisted(() => ({
guard: vi.fn(),
create: vi.fn(),
list: vi.fn(),
cancel: vi.fn(),
after: vi.fn(),
ping: vi.fn(),
source: vi.fn(),
@@ -25,15 +26,17 @@ vi.mock("@/lib/redis", () => ({ redis: { ping: mocks.ping } }));
vi.mock("@/lib/services/furni-job-worker", () => ({
drainFurnitureImports: vi.fn(),
}));
vi.mock("@/lib/services/furni-job-store", () => ({
vi.mock("@/lib/services/furni-job-store", async (original) => ({
...(await original<typeof import("@/lib/services/furni-job-store")>()),
ImportJobStore: class {
create = mocks.create;
list = mocks.list;
requestCancellation = mocks.cancel;
},
}));
vi.mock("@/lib/services/clone-sources", () => ({ getSource: mocks.source }));
import { GET, POST } from "./route";
import { GET, PATCH, POST } from "./route";
const item = {
id: 1,
@@ -106,11 +109,23 @@ it("rejects a missing configured source", async () => {
).status,
).toBe(404);
});
it("only returns the current operators history", async () => {
mocks.list.mockResolvedValue([
{ id: "a", userId: 7 },
{ id: "b", userId: 9 },
]);
it("requests bounded owner-scoped history from the store", async () => {
mocks.list.mockResolvedValue([{ id: "a", userId: 7 }]);
const response = await GET(request({}), ctx);
expect(mocks.list).toHaveBeenCalledWith({ userId: 7, limit: 30 });
expect((await response.json()).jobs).toEqual([{ id: "a", userId: 7 }]);
});
it("requests cancellation with the authenticated owner", async () => {
const id = randomUUID();
mocks.cancel.mockResolvedValue({ id, cancelRequested: true });
const response = await PATCH(request({ id }), ctx);
expect(response.status).toBe(200);
expect(mocks.cancel).toHaveBeenCalledWith(id, 7);
});
it("does not reveal other owners' jobs", async () => {
mocks.cancel.mockResolvedValue(null);
expect((await PATCH(request({ id: randomUUID() }), ctx)).status).toBe(404);
});
it("rejects unsafe cancellation IDs", async () => {
expect((await PATCH(request({ id: "../other" }), ctx)).status).toBe(400);
});
+20 -5
View File
@@ -6,7 +6,7 @@ import { validateClassnames } from "@/lib/furni/studio-inspection";
import { PERMS } from "@/lib/permission-slugs";
import { redis } from "@/lib/redis";
import { getSource } from "@/lib/services/clone-sources";
import { ImportJobStore } from "@/lib/services/furni-job-store";
import { ImportJobStore, validJobId } from "@/lib/services/furni-job-store";
import { drainFurnitureImports } from "@/lib/services/furni-job-worker";
const schema = z.object({
@@ -38,10 +38,10 @@ export const GET = withAdmin(
{ permission: PERMS.ASSETS_IMPORT },
async (_request, ctx) => {
after(drainFurnitureImports);
const jobs = (await new ImportJobStore().list())
.filter((job) => job.userId === ctx.session.user.id)
.reverse()
.slice(0, 30);
const jobs = await new ImportJobStore().list({
userId: ctx.session.user.id,
limit: 30,
});
return apiOk({ jobs });
},
);
@@ -82,3 +82,18 @@ export const POST = withAdmin(
return apiOk({ job });
},
);
export const PATCH = withAdmin(
{ permission: PERMS.ASSETS_IMPORT },
async (request, ctx) => {
const body = await request.json().catch(() => null);
if (!validJobId(body?.id)) return apiError("Invalid import ID", 400);
const job = await new ImportJobStore().requestCancellation(
body.id,
ctx.session.user.id,
);
if (!job) return apiError("Import job not found", 404);
after(drainFurnitureImports);
return apiOk({ job });
},
);