This commit is contained in:
Your Name committed 2026-07-25 03:02:07 -04:00
1 parent 9a915ee395
commit f64fee1347
2 files changed
+203 -20

No files matched your search

+106 -4
View File
@@ -9,8 +9,11 @@ import {
isValidMenuUrl, isValidMenuUrl,
lockMenuForTransaction, lockMenuForTransaction,
lockMenuNameForTransaction, lockMenuNameForTransaction,
MenuBusyError,
menusRoute, menusRoute,
prepareMenuForAdminWrite,
resolveMenuItemLabel, resolveMenuItemLabel,
runAddMenuTransaction,
} from "./menus"; } from "./menus";
const menu = { const menu = {
@@ -135,11 +138,11 @@ describe("footer menu helpers", () => {
$queryRaw: async (strings: TemplateStringsArray, ...parameters: unknown[]) => { $queryRaw: async (strings: TemplateStringsArray, ...parameters: unknown[]) => {
query = strings.join("?"); query = strings.join("?");
values = parameters; values = parameters;
return []; return [{ locked: true }];
}, },
}, menu.id); }, menu.id);
expect(query).toBe("SELECT pg_advisory_xact_lock(?)"); expect(query).toBe("SELECT pg_try_advisory_xact_lock(?) AS locked");
expect(values).toEqual([menu.id]); expect(values).toEqual([menu.id]);
}); });
@@ -150,14 +153,113 @@ describe("footer menu helpers", () => {
$queryRaw: async (strings: TemplateStringsArray, ...parameters: unknown[]) => { $queryRaw: async (strings: TemplateStringsArray, ...parameters: unknown[]) => {
query = strings.join("?"); query = strings.join("?");
values = parameters; values = parameters;
return []; return [{ locked: true }];
}, },
}, "footer-explore"); }, "footer-explore");
expect(query).toBe("SELECT pg_advisory_xact_lock(1213025877, hashtext(?))"); expect(query).toBe("SELECT pg_try_advisory_xact_lock(1213025877, hashtext(?)) AS locked");
expect(values).toEqual(["footer-explore"]); expect(values).toEqual(["footer-explore"]);
}); });
test("rejects a false name lock without invoking subsequent work", async () => {
let writes = 0;
const operation = async () => {
await lockMenuNameForTransaction({
$queryRaw: async () => [{ locked: false }],
}, "footer-explore");
writes += 1;
};
await expect(operation()).rejects.toBeInstanceOf(MenuBusyError);
expect(writes).toBe(0);
});
test("rejects a false ID lock so an enclosing bootstrap transaction can rollback", async () => {
let writes = 0;
const operation = async () => {
await lockMenuForTransaction({
$queryRaw: async () => [{ locked: false }],
}, menu.id);
writes += 1;
};
await expect(operation()).rejects.toBeInstanceOf(MenuBusyError);
expect(writes).toBe(0);
});
test("rolls back a bootstrapped menu when the ID lock is busy", async () => {
let persistedMenu = false;
let writes = 0;
let lockCalls = 0;
const db = {
$transaction: async (callback: (tx: any) => Promise<unknown>) => {
let transactionCreatedMenu = false;
const tx = {
$queryRaw: async () => {
lockCalls += 1;
return [{ locked: lockCalls === 1 }];
},
menu: {
findUnique: async () => null,
create: async () => {
transactionCreatedMenu = true;
return { id: 99 };
},
},
};
try {
const result = await callback(tx);
persistedMenu = transactionCreatedMenu;
return result;
} catch (error) {
persistedMenu = false;
throw error;
}
},
};
const result = await runAddMenuTransaction(
"footer-explore",
null,
async () => {
writes += 1;
},
db,
);
expect(result).toEqual({ busy: true });
expect(lockCalls).toBe(2);
expect(persistedMenu).toBe(false);
expect(writes).toBe(0);
});
test("does not update existing Admin menu metadata when the ID lock is busy", async () => {
let lockCalls = 0;
let updates = 0;
const tx = {
$queryRaw: async () => {
lockCalls += 1;
return [{ locked: lockCalls === 1 }];
},
menu: {
findUnique: async () => ({ id: menu.id }),
create: async () => {
throw new Error("Existing menu must not be created.");
},
update: async () => {
updates += 1;
return { id: menu.id };
},
},
};
await expect(
prepareMenuForAdminWrite(tx, menu.name, { isActive: false }),
).rejects.toBeInstanceOf(MenuBusyError);
expect(lockCalls).toBe(2);
expect(updates).toBe(0);
});
test("returns the compact empty read contract for an absent footer menu", () => { test("returns the compact empty read contract for an absent footer menu", () => {
expect(compactMissingFooterMenu("footer-support", "zh")).toEqual({ expect(compactMissingFooterMenu("footer-support", "zh")).toEqual({
name: "footer-support", name: "footer-support",
+97 -16
View File
@@ -111,12 +111,62 @@ const getFooterMenu = async (
include: footerMenuInclude, include: footerMenuInclude,
}); });
export class MenuBusyError extends Error {
constructor() {
super("Menu is currently being edited.");
this.name = "MenuBusyError";
}
}
const assertAdvisoryLock = (rows: unknown) => {
if (
!Array.isArray(rows)
|| rows.length === 0
|| (rows[0] as { locked?: unknown }).locked !== true
) {
throw new MenuBusyError();
}
};
export const lockMenuForTransaction = async (tx: any, menuId: number) => { export const lockMenuForTransaction = async (tx: any, menuId: number) => {
await tx.$queryRaw`SELECT pg_advisory_xact_lock(${menuId})`; const rows = await tx.$queryRaw`SELECT pg_try_advisory_xact_lock(${menuId}) AS locked`;
assertAdvisoryLock(rows);
return true;
}; };
export const lockMenuNameForTransaction = async (tx: any, name: FooterMenuName | string) => { export const lockMenuNameForTransaction = async (tx: any, name: FooterMenuName | string) => {
await tx.$queryRaw`SELECT pg_advisory_xact_lock(1213025877, hashtext(${name}))`; const rows = await tx.$queryRaw`SELECT pg_try_advisory_xact_lock(1213025877, hashtext(${name})) AS locked`;
assertAdvisoryLock(rows);
return true;
};
export const prepareMenuForAdminWrite = async (
tx: any,
name: string,
updateData: Record<string, unknown>,
) => {
await lockMenuNameForTransaction(tx, name);
let identity = await tx.menu.findUnique({
where: { name },
select: { id: true },
});
if (!identity) {
identity = await tx.menu.create({
data: { name },
select: { id: true },
});
}
await lockMenuForTransaction(tx, identity.id);
let menu = await tx.menu.findUnique({ where: { name } });
if (!menu) throw new Error(`Menu ${name} disappeared before Admin write.`);
if (Object.keys(updateData).length > 0) {
menu = await tx.menu.update({
where: { name },
data: updateData,
});
}
return menu;
}; };
export async function executeVersionedMenuWrite<T>( export async function executeVersionedMenuWrite<T>(
@@ -162,8 +212,9 @@ const runMenuTransaction = async <T>(
name: FooterMenuName, name: FooterMenuName,
expectedVersion: unknown, expectedVersion: unknown,
write: (tx: any, menu: VersionedMenu) => Promise<T>, write: (tx: any, menu: VersionedMenu) => Promise<T>,
) => ) => {
prisma.$transaction(async (tx: any) => { try {
return await prisma.$transaction(async (tx: any) => {
await lockMenuNameForTransaction(tx, name); await lockMenuNameForTransaction(tx, name);
const identity = await tx.menu.findUnique({ const identity = await tx.menu.findUnique({
where: { name }, where: { name },
@@ -181,13 +232,20 @@ const runMenuTransaction = async <T>(
return { value, version: fingerprintMenu(updated) }; return { value, version: fingerprintMenu(updated) };
}); });
}); });
} catch (error) {
if (error instanceof MenuBusyError) return { busy: true as const };
throw error;
}
};
const runAddMenuTransaction = async ( export const runAddMenuTransaction = async (
name: FooterMenuName, name: FooterMenuName,
expectedVersion: unknown, expectedVersion: unknown,
write: (tx: any, menu: VersionedMenu) => Promise<unknown>, write: (tx: any, menu: VersionedMenu) => Promise<unknown>,
) => db: any = prisma,
prisma.$transaction(async (tx: any) => { ) => {
try {
return await db.$transaction(async (tx: any) => {
await lockMenuNameForTransaction(tx, name); await lockMenuNameForTransaction(tx, name);
let identity = await tx.menu.findUnique({ let identity = await tx.menu.findUnique({
where: { name }, where: { name },
@@ -222,12 +280,27 @@ const runAddMenuTransaction = async (
}, },
); );
}); });
} catch (error) {
if (error instanceof MenuBusyError) return { busy: true as const };
throw error;
}
};
const unwrapTransaction = ( const unwrapTransaction = (
result: Awaited<ReturnType<typeof runMenuTransaction>>, result: Awaited<ReturnType<typeof runMenuTransaction>>,
name: FooterMenuName, name: FooterMenuName,
set: { status?: number | string }, set: { status?: number | string },
) => { ) => {
if ("busy" in result) {
return {
error: errorResponse(
set,
409,
"MENU_BUSY",
"Menu is currently being edited. Read it again and retry.",
),
};
}
if ("missing" in result) { if ("missing" in result) {
return { error: errorResponse(set, 404, "MENU_NOT_FOUND", `Menu ${name} was not found.`) }; return { error: errorResponse(set, 404, "MENU_NOT_FOUND", `Menu ${name} was not found.`) };
} }
@@ -410,6 +483,14 @@ export const menusRoute = new Elysia({ prefix: "/menus" })
include: { translations: true }, include: { translations: true },
}), }),
); );
if ("busy" in transaction) {
return errorResponse(
set,
409,
"MENU_BUSY",
"Menu is currently being edited. Read it again and retry.",
);
}
if (!transaction.written) { if (!transaction.written) {
return errorResponse( return errorResponse(
set, set,
@@ -732,15 +813,7 @@ export const menusRoute = new Elysia({ prefix: "/menus" })
if (rootImage !== undefined) updateData.rootImage = rootImage; if (rootImage !== undefined) updateData.rootImage = rootImage;
return await prisma.$transaction(async (tx: any) => { return await prisma.$transaction(async (tx: any) => {
await lockMenuNameForTransaction(tx, name); const menu = await prepareMenuForAdminWrite(tx, name, updateData);
let menu = await tx.menu.findUnique({ where: { name } });
if (!menu) {
menu = await tx.menu.create({ data: { name, ...updateData } });
} else if (Object.keys(updateData).length > 0) {
menu = await tx.menu.update({ where: { name }, data: updateData });
}
await lockMenuForTransaction(tx, menu.id);
const existingItems = await tx.menuItem.findMany({ where: { menuId: menu.id } }); const existingItems = await tx.menuItem.findMany({ where: { menuId: menu.id } });
const existingItemIds = existingItems.map((i: any) => i.id); const existingItemIds = existingItems.map((i: any) => i.id);
const payloadItemIds = items.map(i => i.id).filter(id => id != null); const payloadItemIds = items.map(i => i.id).filter(id => id != null);
@@ -796,6 +869,14 @@ export const menusRoute = new Elysia({ prefix: "/menus" })
}); });
}); });
} catch (err) { } catch (err) {
if (err instanceof MenuBusyError) {
return errorResponse(
set,
409,
"MENU_BUSY",
"Menu is currently being edited. Read it again and retry.",
);
}
console.error(err); console.error(err);
set.status = 500; set.status = 500;
return { error: "Failed to update menu" }; return { error: "Failed to update menu" };