141 lines
4.1 KiB
TypeScript
141 lines
4.1 KiB
TypeScript
import { after } from "next/server";
|
|
import { z } from "zod";
|
|
import { apiError, apiOk } from "@/lib/api";
|
|
import { withAdmin } from "@/lib/api-handler";
|
|
import { getRequestId } from "@/lib/foundation/request-context";
|
|
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, validJobId } from "@/lib/services/furni-job-store";
|
|
import { drainFurnitureImports } from "@/lib/services/furni-job-worker";
|
|
|
|
const schema = z.object({
|
|
mode: z.literal("repair").optional(),
|
|
id: z.uuid().refine((value) => value[14] === "4"),
|
|
sourceId: z.string().max(100).optional(),
|
|
translate: z.boolean().default(false),
|
|
langs: z
|
|
.array(z.string().regex(/^[a-z]{2,3}$/))
|
|
.max(25)
|
|
.optional(),
|
|
items: z
|
|
.array(
|
|
z.object({
|
|
id: z.number().int().nonnegative(),
|
|
classname: z.string(),
|
|
name: z.string().min(1).max(500),
|
|
description: z.string().max(5000),
|
|
type: z.enum(["flooritem", "wallitem"]),
|
|
revision: z.number().int().nonnegative(),
|
|
category: z.string().max(200),
|
|
attachmentId: z.uuid().optional(),
|
|
}),
|
|
)
|
|
.min(1)
|
|
.max(500),
|
|
});
|
|
export const GET = withAdmin(
|
|
{ permission: PERMS.ASSETS_IMPORT },
|
|
async (request, ctx) => {
|
|
const query = new URL(request.url).searchParams;
|
|
if (query.has("page")) {
|
|
const before = query.get("before") || undefined;
|
|
if (
|
|
before &&
|
|
!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\|[0-9a-f-]{36}$/i.test(
|
|
before,
|
|
)
|
|
)
|
|
return apiError("Invalid history cursor", 400);
|
|
after(drainFurnitureImports);
|
|
return apiOk(
|
|
await new ImportJobStore().page({
|
|
userId: ctx.session.user.id,
|
|
limit: 20,
|
|
before,
|
|
}),
|
|
);
|
|
}
|
|
after(drainFurnitureImports);
|
|
const jobs = await new ImportJobStore().list({
|
|
userId: ctx.session.user.id,
|
|
limit: 30,
|
|
});
|
|
return apiOk({ jobs });
|
|
},
|
|
);
|
|
export const POST = withAdmin(
|
|
{ permission: PERMS.ASSETS_IMPORT },
|
|
async (request, ctx) => {
|
|
const parsed = schema.safeParse(await request.json().catch(() => null));
|
|
if (
|
|
!parsed.success ||
|
|
!validateClassnames(parsed.data.items.map((item) => item.classname))
|
|
)
|
|
return apiError(
|
|
"Invalid furniture import request (maximum 500 items)",
|
|
400,
|
|
);
|
|
if (!redis || (await redis.ping().catch(() => null)) !== "PONG")
|
|
return apiError(
|
|
"Background import queue is temporarily unavailable. Please retry shortly.",
|
|
503,
|
|
);
|
|
if (parsed.data.sourceId && !(await getSource(parsed.data.sourceId)))
|
|
return apiError("Source not found", 404);
|
|
const store = new ImportJobStore(),
|
|
body = parsed.data;
|
|
const createdAt = new Date().toISOString();
|
|
const unique = [
|
|
...new Map(body.items.map((item) => [item.classname, item])).values(),
|
|
];
|
|
const job = await store.create({
|
|
...body,
|
|
userId: ctx.session.user.id,
|
|
operationId: getRequestId(),
|
|
createdAt,
|
|
updatedAt: createdAt,
|
|
state: "queued",
|
|
items: unique.map((item) => ({ ...item, state: "pending" })),
|
|
});
|
|
after(drainFurnitureImports);
|
|
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);
|
|
if (body.action === "retry") {
|
|
if (!redis || (await redis.ping().catch(() => null)) !== "PONG")
|
|
return apiError(
|
|
"Background import queue is temporarily unavailable",
|
|
503,
|
|
);
|
|
const job = await new ImportJobStore().retry(
|
|
body.id,
|
|
ctx.session.user.id,
|
|
);
|
|
if (!job)
|
|
return apiError(
|
|
"No safe remaining items to retry, or import job not found",
|
|
404,
|
|
);
|
|
after(drainFurnitureImports);
|
|
return apiOk({ job });
|
|
}
|
|
if (body.action && body.action !== "cancel")
|
|
return apiError("Invalid job action", 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 });
|
|
},
|
|
);
|