fix: prevent stale drawing saves
This commit is contained in:
+43
-16
@@ -1,4 +1,5 @@
|
||||
import { type DrawingStore } from "./db";
|
||||
import { type DrawingMeta } from "./shared";
|
||||
import { HttpError, normalizeTitle, parseJsonBody, parseSceneText } from "./scene";
|
||||
|
||||
type RouteRequest = Request & {
|
||||
@@ -22,6 +23,29 @@ function errorResponse(error: unknown): Response {
|
||||
return json({ ok: false, error: "Internal server error" }, { status: 500 });
|
||||
}
|
||||
|
||||
function toMeta(drawing: DrawingMeta): DrawingMeta {
|
||||
return {
|
||||
id: drawing.id,
|
||||
title: drawing.title,
|
||||
createdAt: drawing.createdAt,
|
||||
updatedAt: drawing.updatedAt,
|
||||
revision: drawing.revision,
|
||||
};
|
||||
}
|
||||
|
||||
function parseExpectedRevision(raw: Record<string, unknown>): number | undefined {
|
||||
if (!Object.hasOwn(raw, "expectedRevision")) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const value = raw.expectedRevision;
|
||||
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
|
||||
throw new HttpError(400, "expectedRevision must be a non-negative integer");
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
export function createApi(store: DrawingStore) {
|
||||
return {
|
||||
health() {
|
||||
@@ -36,10 +60,7 @@ export function createApi(store: DrawingStore) {
|
||||
const drawing = store.createDrawing();
|
||||
return json(
|
||||
{
|
||||
id: drawing.id,
|
||||
title: drawing.title,
|
||||
createdAt: drawing.createdAt,
|
||||
updatedAt: drawing.updatedAt,
|
||||
...toMeta(drawing),
|
||||
...drawing.scene,
|
||||
},
|
||||
{ status: 201 },
|
||||
@@ -55,10 +76,7 @@ export function createApi(store: DrawingStore) {
|
||||
}
|
||||
|
||||
return json({
|
||||
id: drawing.id,
|
||||
title: drawing.title,
|
||||
createdAt: drawing.createdAt,
|
||||
updatedAt: drawing.updatedAt,
|
||||
...toMeta(drawing),
|
||||
...drawing.scene,
|
||||
});
|
||||
},
|
||||
@@ -77,29 +95,38 @@ export function createApi(store: DrawingStore) {
|
||||
Object.hasOwn(raw, "elements") ||
|
||||
Object.hasOwn(raw, "appState") ||
|
||||
Object.hasOwn(raw, "files");
|
||||
const expectedRevision = parseExpectedRevision(raw);
|
||||
|
||||
if (!hasTitle && !hasScene) {
|
||||
throw new HttpError(400, "Nothing to update");
|
||||
}
|
||||
|
||||
const scene = hasScene ? parseSceneText(text) : undefined;
|
||||
const updated = store.updateDrawing(id, {
|
||||
const result = store.updateDrawing(id, {
|
||||
scene,
|
||||
title: hasTitle ? normalizeTitle(raw.title) : undefined,
|
||||
expectedRevision,
|
||||
});
|
||||
|
||||
if (!updated) {
|
||||
if (!result.ok && result.reason === "not_found") {
|
||||
return json({ ok: false, error: "Drawing not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
if (!result.ok && result.reason === "conflict") {
|
||||
return json(
|
||||
{
|
||||
ok: false,
|
||||
error: "Drawing changed externally",
|
||||
drawing: toMeta(result.drawing),
|
||||
},
|
||||
{ status: 409 },
|
||||
);
|
||||
}
|
||||
|
||||
const updated = result.drawing;
|
||||
return json({
|
||||
ok: true,
|
||||
drawing: {
|
||||
id: updated.id,
|
||||
title: updated.title,
|
||||
createdAt: updated.createdAt,
|
||||
updatedAt: updated.updatedAt,
|
||||
},
|
||||
drawing: toMeta(updated),
|
||||
});
|
||||
} catch (error) {
|
||||
return errorResponse(error);
|
||||
|
||||
Reference in New Issue
Block a user