x
This commit is contained in:
1 parent
167d07f0c2
commit
9a915ee395
6 files changed
+259
-35
No files matched your search
@@ -190,6 +190,8 @@ Use `get_page_section` with `lang=all` only when every stored language map is ge
|
|||||||
|
|
||||||
Use `update_footer_menu_item_translation` for one language at a time. Use `reorder_footer_menu_items` only with the complete list of current item IDs. If the backend returns `MENU_VERSION_CONFLICT`, discard the old version and read the menu again.
|
Use `update_footer_menu_item_translation` for one language at a time. Use `reorder_footer_menu_items` only with the complete list of current item IDs. If the backend returns `MENU_VERSION_CONFLICT`, discard the old version and read the menu again.
|
||||||
|
|
||||||
|
An allowed Footer menu may not exist before its first item is saved. In that case, `get_footer_menu` returns `exists=false` and `version=null`. Run `add_footer_menu_item` with `dryRun=true`; a `willCreateMenu=true` preview confirms that the actual add will create the active menu and first item atomically.
|
||||||
|
|
||||||
Do not use legacy `update_footer_content` for menu arrays. It remains available only for the Footer brand description and social links. Website Collections come from Categories, not a Footer menu.
|
Do not use legacy `update_footer_content` for menu arrays. It remains available only for the Footer brand description and social links. Website Collections come from Categories, not a Footer menu.
|
||||||
|
|
||||||
## What to Expect
|
## What to Expect
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import {
|
import {
|
||||||
|
buildFooterMenuAddDryRun,
|
||||||
|
canBootstrapFooterMenu,
|
||||||
|
compactMissingFooterMenu,
|
||||||
createMenuTranslations,
|
createMenuTranslations,
|
||||||
executeVersionedMenuWrite,
|
executeVersionedMenuWrite,
|
||||||
fingerprintMenu,
|
fingerprintMenu,
|
||||||
isValidMenuUrl,
|
isValidMenuUrl,
|
||||||
lockMenuForTransaction,
|
lockMenuForTransaction,
|
||||||
|
lockMenuNameForTransaction,
|
||||||
menusRoute,
|
menusRoute,
|
||||||
resolveMenuItemLabel,
|
resolveMenuItemLabel,
|
||||||
} from "./menus";
|
} from "./menus";
|
||||||
@@ -138,6 +142,59 @@ describe("footer menu helpers", () => {
|
|||||||
expect(query).toBe("SELECT pg_advisory_xact_lock(?)");
|
expect(query).toBe("SELECT pg_advisory_xact_lock(?)");
|
||||||
expect(values).toEqual([menu.id]);
|
expect(values).toEqual([menu.id]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("takes a transaction-scoped advisory lock keyed by menu name", async () => {
|
||||||
|
let query = "";
|
||||||
|
let values: unknown[] = [];
|
||||||
|
await lockMenuNameForTransaction({
|
||||||
|
$queryRaw: async (strings: TemplateStringsArray, ...parameters: unknown[]) => {
|
||||||
|
query = strings.join("?");
|
||||||
|
values = parameters;
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
}, "footer-explore");
|
||||||
|
|
||||||
|
expect(query).toBe("SELECT pg_advisory_xact_lock(1213025877, hashtext(?))");
|
||||||
|
expect(values).toEqual(["footer-explore"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns the compact empty read contract for an absent footer menu", () => {
|
||||||
|
expect(compactMissingFooterMenu("footer-support", "zh")).toEqual({
|
||||||
|
name: "footer-support",
|
||||||
|
lang: "zh",
|
||||||
|
exists: false,
|
||||||
|
version: null,
|
||||||
|
items: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("permits bootstrap only when expectedVersion is omitted or null", () => {
|
||||||
|
expect(canBootstrapFooterMenu(undefined)).toBe(true);
|
||||||
|
expect(canBootstrapFooterMenu(null)).toBe(true);
|
||||||
|
expect(canBootstrapFooterMenu("sha256:stale")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("previews first-item bootstrap without creating menu state", () => {
|
||||||
|
expect(buildFooterMenuAddDryRun(null, {
|
||||||
|
label: "Contact",
|
||||||
|
url: "mailto:hello@example.com",
|
||||||
|
translations: { th: "ติดต่อเรา" },
|
||||||
|
})).toEqual({
|
||||||
|
success: true,
|
||||||
|
dryRun: true,
|
||||||
|
operation: "add",
|
||||||
|
version: null,
|
||||||
|
willCreateMenu: true,
|
||||||
|
item: {
|
||||||
|
label: "Contact",
|
||||||
|
url: "mailto:hello@example.com",
|
||||||
|
order: 0,
|
||||||
|
columnImage: null,
|
||||||
|
hoverImage: null,
|
||||||
|
translations: { th: "ติดต่อเรา" },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("footer menu route validation", () => {
|
describe("footer menu route validation", () => {
|
||||||
|
|||||||
@@ -115,6 +115,10 @@ export const lockMenuForTransaction = async (tx: any, menuId: number) => {
|
|||||||
await tx.$queryRaw`SELECT pg_advisory_xact_lock(${menuId})`;
|
await tx.$queryRaw`SELECT pg_advisory_xact_lock(${menuId})`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const lockMenuNameForTransaction = async (tx: any, name: FooterMenuName | string) => {
|
||||||
|
await tx.$queryRaw`SELECT pg_advisory_xact_lock(1213025877, hashtext(${name}))`;
|
||||||
|
};
|
||||||
|
|
||||||
export async function executeVersionedMenuWrite<T>(
|
export async function executeVersionedMenuWrite<T>(
|
||||||
menu: VersionedMenu,
|
menu: VersionedMenu,
|
||||||
expectedVersion: unknown,
|
expectedVersion: unknown,
|
||||||
@@ -160,6 +164,7 @@ const runMenuTransaction = async <T>(
|
|||||||
write: (tx: any, menu: VersionedMenu) => Promise<T>,
|
write: (tx: any, menu: VersionedMenu) => Promise<T>,
|
||||||
) =>
|
) =>
|
||||||
prisma.$transaction(async (tx: any) => {
|
prisma.$transaction(async (tx: any) => {
|
||||||
|
await lockMenuNameForTransaction(tx, name);
|
||||||
const identity = await tx.menu.findUnique({
|
const identity = await tx.menu.findUnique({
|
||||||
where: { name },
|
where: { name },
|
||||||
select: { id: true },
|
select: { id: true },
|
||||||
@@ -177,6 +182,47 @@ const runMenuTransaction = async <T>(
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const runAddMenuTransaction = async (
|
||||||
|
name: FooterMenuName,
|
||||||
|
expectedVersion: unknown,
|
||||||
|
write: (tx: any, menu: VersionedMenu) => Promise<unknown>,
|
||||||
|
) =>
|
||||||
|
prisma.$transaction(async (tx: any) => {
|
||||||
|
await lockMenuNameForTransaction(tx, name);
|
||||||
|
let identity = await tx.menu.findUnique({
|
||||||
|
where: { name },
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
const createdMenu = !identity;
|
||||||
|
if (!identity) {
|
||||||
|
if (expectedVersion !== undefined && expectedVersion !== null) {
|
||||||
|
return { written: false as const, currentVersion: null };
|
||||||
|
}
|
||||||
|
identity = await tx.menu.create({
|
||||||
|
data: { name, isActive: true },
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await lockMenuForTransaction(tx, identity.id);
|
||||||
|
const menu = await getFooterMenu(name, tx);
|
||||||
|
if (!menu) throw new Error(`Menu ${name} disappeared during bootstrap transaction.`);
|
||||||
|
return executeVersionedMenuWrite(
|
||||||
|
menu,
|
||||||
|
createdMenu ? undefined : expectedVersion,
|
||||||
|
async () => {
|
||||||
|
const value = await write(tx, menu);
|
||||||
|
const updated = await getFooterMenu(name, tx);
|
||||||
|
if (!updated) throw new Error(`Menu ${name} disappeared during add transaction.`);
|
||||||
|
return {
|
||||||
|
value,
|
||||||
|
version: fingerprintMenu(updated),
|
||||||
|
createdMenu,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
const unwrapTransaction = (
|
const unwrapTransaction = (
|
||||||
result: Awaited<ReturnType<typeof runMenuTransaction>>,
|
result: Awaited<ReturnType<typeof runMenuTransaction>>,
|
||||||
name: FooterMenuName,
|
name: FooterMenuName,
|
||||||
@@ -229,6 +275,46 @@ const compactMenu = (
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const compactMissingFooterMenu = (
|
||||||
|
name: FooterMenuName,
|
||||||
|
lang: MenuLanguage | "all",
|
||||||
|
) => ({
|
||||||
|
name,
|
||||||
|
lang,
|
||||||
|
exists: false as const,
|
||||||
|
version: null,
|
||||||
|
items: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
export const canBootstrapFooterMenu = (expectedVersion: unknown) =>
|
||||||
|
expectedVersion === undefined || expectedVersion === null;
|
||||||
|
|
||||||
|
export const buildFooterMenuAddDryRun = (
|
||||||
|
menu: VersionedMenu | null,
|
||||||
|
input: {
|
||||||
|
label: string;
|
||||||
|
url: string;
|
||||||
|
order?: number;
|
||||||
|
columnImage?: string | null;
|
||||||
|
hoverImage?: string | null;
|
||||||
|
translations?: Record<string, string>;
|
||||||
|
},
|
||||||
|
) => ({
|
||||||
|
success: true,
|
||||||
|
dryRun: true,
|
||||||
|
operation: "add",
|
||||||
|
version: menu ? fingerprintMenu(menu) : null,
|
||||||
|
willCreateMenu: !menu,
|
||||||
|
item: {
|
||||||
|
label: input.label,
|
||||||
|
url: input.url,
|
||||||
|
order: input.order ?? menu?.items.length ?? 0,
|
||||||
|
columnImage: input.columnImage ?? null,
|
||||||
|
hoverImage: input.hoverImage ?? null,
|
||||||
|
translations: input.translations ?? {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export const menusRoute = new Elysia({ prefix: "/menus" })
|
export const menusRoute = new Elysia({ prefix: "/menus" })
|
||||||
.get("/", async () => {
|
.get("/", async () => {
|
||||||
return await prisma.menu.findMany({
|
return await prisma.menu.findMany({
|
||||||
@@ -250,9 +336,9 @@ export const menusRoute = new Elysia({ prefix: "/menus" })
|
|||||||
}
|
}
|
||||||
const menu = await getFooterMenu(name);
|
const menu = await getFooterMenu(name);
|
||||||
if (!menu) {
|
if (!menu) {
|
||||||
return errorResponse(set, 404, "MENU_NOT_FOUND", `Menu ${name} was not found.`);
|
return compactMissingFooterMenu(name, lang);
|
||||||
}
|
}
|
||||||
return compactMenu(menu, lang, query.fallback !== "false");
|
return { ...compactMenu(menu, lang, query.fallback !== "false"), exists: true };
|
||||||
})
|
})
|
||||||
.get("/:name", async ({ params: { name }, set }) => {
|
.get("/:name", async ({ params: { name }, set }) => {
|
||||||
const menu = await prisma.menu.findUnique({
|
const menu = await prisma.menu.findUnique({
|
||||||
@@ -283,28 +369,33 @@ export const menusRoute = new Elysia({ prefix: "/menus" })
|
|||||||
return errorResponse(set, 400, "INVALID_LOCALE", parsedTranslations.error);
|
return errorResponse(set, 400, "INVALID_LOCALE", parsedTranslations.error);
|
||||||
}
|
}
|
||||||
const menu = await getFooterMenu(name);
|
const menu = await getFooterMenu(name);
|
||||||
if (!menu) return errorResponse(set, 404, "MENU_NOT_FOUND", `Menu ${name} was not found.`);
|
if (!menu && !canBootstrapFooterMenu(input.expectedVersion)) {
|
||||||
const versionCheck = checkVersion(menu, input.expectedVersion, set);
|
return errorResponse(
|
||||||
if (versionCheck.conflict) return versionCheck.conflict;
|
set,
|
||||||
|
409,
|
||||||
|
"MENU_VERSION_CONFLICT",
|
||||||
|
"Menu does not exist, so the supplied version is stale.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const versionCheck = menu
|
||||||
|
? checkVersion(menu, input.expectedVersion, set)
|
||||||
|
: { currentVersion: null };
|
||||||
|
if ("conflict" in versionCheck && versionCheck.conflict) return versionCheck.conflict;
|
||||||
|
|
||||||
const preview = {
|
const preview = {
|
||||||
label: input.label,
|
label: input.label,
|
||||||
url: input.url,
|
url: input.url,
|
||||||
order: input.order ?? menu.items.length,
|
order: input.order ?? menu?.items.length ?? 0,
|
||||||
columnImage: input.columnImage ?? null,
|
columnImage: input.columnImage ?? null,
|
||||||
hoverImage: input.hoverImage ?? null,
|
hoverImage: input.hoverImage ?? null,
|
||||||
};
|
};
|
||||||
if (input.dryRun === true) {
|
if (input.dryRun === true) {
|
||||||
return {
|
return buildFooterMenuAddDryRun(menu, input);
|
||||||
success: true,
|
|
||||||
dryRun: true,
|
|
||||||
operation: "add",
|
|
||||||
version: versionCheck.currentVersion,
|
|
||||||
item: { ...preview, translations: input.translations ?? {} },
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
const transaction = unwrapTransaction(
|
const transaction = await runAddMenuTransaction(
|
||||||
await runMenuTransaction(name, input.expectedVersion, (tx, currentMenu) =>
|
name,
|
||||||
|
input.expectedVersion,
|
||||||
|
(tx, currentMenu) =>
|
||||||
tx.menuItem.create({
|
tx.menuItem.create({
|
||||||
data: {
|
data: {
|
||||||
menuId: currentMenu.id,
|
menuId: currentMenu.id,
|
||||||
@@ -317,16 +408,24 @@ export const menusRoute = new Elysia({ prefix: "/menus" })
|
|||||||
: undefined,
|
: undefined,
|
||||||
},
|
},
|
||||||
include: { translations: true },
|
include: { translations: true },
|
||||||
})),
|
}),
|
||||||
name,
|
|
||||||
set,
|
|
||||||
);
|
);
|
||||||
if (transaction.error) return transaction.error;
|
if (!transaction.written) {
|
||||||
|
return errorResponse(
|
||||||
|
set,
|
||||||
|
409,
|
||||||
|
"MENU_VERSION_CONFLICT",
|
||||||
|
transaction.currentVersion === null
|
||||||
|
? "Menu does not exist, so the supplied version is stale."
|
||||||
|
: "Menu changed after it was read. Fetch the menu again before writing.",
|
||||||
|
);
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
dryRun: false,
|
dryRun: false,
|
||||||
item: transaction.value!.value,
|
createdMenu: transaction.value.createdMenu,
|
||||||
version: transaction.value!.version,
|
item: transaction.value.value,
|
||||||
|
version: transaction.value.version,
|
||||||
};
|
};
|
||||||
}, {
|
}, {
|
||||||
body: t.Object({
|
body: t.Object({
|
||||||
@@ -337,7 +436,7 @@ export const menusRoute = new Elysia({ prefix: "/menus" })
|
|||||||
hoverImage: t.Optional(t.Nullable(t.String())),
|
hoverImage: t.Optional(t.Nullable(t.String())),
|
||||||
translations: t.Optional(t.Record(t.String(), t.String({ minLength: 1 }))),
|
translations: t.Optional(t.Record(t.String(), t.String({ minLength: 1 }))),
|
||||||
dryRun: t.Optional(t.Boolean()),
|
dryRun: t.Optional(t.Boolean()),
|
||||||
expectedVersion: t.Optional(t.String()),
|
expectedVersion: t.Optional(t.Nullable(t.String())),
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
.put("/footer/:name/items/reorder", async ({ params: { name }, body, set }: any) => {
|
.put("/footer/:name/items/reorder", async ({ params: { name }, body, set }: any) => {
|
||||||
@@ -627,22 +726,22 @@ export const menusRoute = new Elysia({ prefix: "/menus" })
|
|||||||
translations?: { langCode: string; label: string }[]
|
translations?: { langCode: string; label: string }[]
|
||||||
}[]
|
}[]
|
||||||
};
|
};
|
||||||
let menu = await prisma.menu.findUnique({ where: { name } });
|
|
||||||
|
|
||||||
const updateData: any = {};
|
const updateData: any = {};
|
||||||
if (payload !== undefined) updateData.payload = payload;
|
if (payload !== undefined) updateData.payload = payload;
|
||||||
if (isActive !== undefined) updateData.isActive = isActive;
|
if (isActive !== undefined) updateData.isActive = isActive;
|
||||||
if (rootImage !== undefined) updateData.rootImage = rootImage;
|
if (rootImage !== undefined) updateData.rootImage = rootImage;
|
||||||
|
|
||||||
|
return await prisma.$transaction(async (tx: any) => {
|
||||||
|
await lockMenuNameForTransaction(tx, name);
|
||||||
|
let menu = await tx.menu.findUnique({ where: { name } });
|
||||||
if (!menu) {
|
if (!menu) {
|
||||||
menu = await prisma.menu.create({ data: { name, ...updateData } });
|
menu = await tx.menu.create({ data: { name, ...updateData } });
|
||||||
} else if (Object.keys(updateData).length > 0) {
|
} else if (Object.keys(updateData).length > 0) {
|
||||||
menu = await prisma.menu.update({ where: { name }, data: updateData });
|
menu = await tx.menu.update({ where: { name }, data: updateData });
|
||||||
}
|
}
|
||||||
|
|
||||||
await prisma.$transaction(async (tx: any) => {
|
await lockMenuForTransaction(tx, menu.id);
|
||||||
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);
|
||||||
const itemsToDelete = existingItemIds.filter((id: number) => !payloadItemIds.includes(id));
|
const itemsToDelete = existingItemIds.filter((id: number) => !payloadItemIds.includes(id));
|
||||||
@@ -675,7 +774,7 @@ export const menusRoute = new Elysia({ prefix: "/menus" })
|
|||||||
} else {
|
} else {
|
||||||
await tx.menuItem.create({
|
await tx.menuItem.create({
|
||||||
data: {
|
data: {
|
||||||
menuId: menu!.id,
|
menuId: menu.id,
|
||||||
label: item.label,
|
label: item.label,
|
||||||
url: item.url,
|
url: item.url,
|
||||||
order: item.order,
|
order: item.order,
|
||||||
@@ -691,12 +790,11 @@ export const menusRoute = new Elysia({ prefix: "/menus" })
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
return tx.menu.findUnique({
|
||||||
|
|
||||||
return await prisma.menu.findUnique({
|
|
||||||
where: { name },
|
where: { name },
|
||||||
include: { items: { orderBy: { order: "asc" }, include: { translations: true } } }
|
include: { items: { orderBy: { order: "asc" }, include: { translations: true } } }
|
||||||
});
|
});
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
set.status = 500;
|
set.status = 500;
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ Use this workflow for every Footer menu edit:
|
|||||||
5. Apply the same operation with `dryRun: false` and the same version.
|
5. Apply the same operation with `dryRun: false` and the same version.
|
||||||
6. Call `get_footer_menu` again to verify and obtain the next version.
|
6. Call `get_footer_menu` again to verify and obtain the next version.
|
||||||
|
|
||||||
|
If an allowed Footer menu does not exist yet, `get_footer_menu` returns `exists: false`, `version: null`, and an empty item list. Previewing the first add returns `willCreateMenu: true`; applying that add creates the menu as active and inserts the first item atomically.
|
||||||
|
|
||||||
Available item-level tools:
|
Available item-level tools:
|
||||||
|
|
||||||
- `add_footer_menu_item`
|
- `add_footer_menu_item`
|
||||||
|
|||||||
@@ -129,6 +129,30 @@ describe("Footer MCP proxy handlers", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("preserves the compact contract for an allowed menu that does not exist", async () => {
|
||||||
|
const { api } = createApi({
|
||||||
|
success: true,
|
||||||
|
menuName: "footer-support",
|
||||||
|
exists: false,
|
||||||
|
version: null,
|
||||||
|
lang: "th",
|
||||||
|
items: [],
|
||||||
|
});
|
||||||
|
const result = await handleMenuTool(api, "get_footer_menu", {
|
||||||
|
menuName: "footer-support",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(output(result)).toEqual({
|
||||||
|
success: true,
|
||||||
|
operation: "get_footer_menu",
|
||||||
|
menuName: "footer-support",
|
||||||
|
exists: false,
|
||||||
|
version: null,
|
||||||
|
lang: "th",
|
||||||
|
items: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test("adds an item with write controls", async () => {
|
test("adds an item with write controls", async () => {
|
||||||
const { api, calls } = createApi();
|
const { api, calls } = createApi();
|
||||||
await handleMenuTool(api, "add_footer_menu_item", {
|
await handleMenuTool(api, "add_footer_menu_item", {
|
||||||
@@ -155,6 +179,45 @@ describe("Footer MCP proxy handlers", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("preserves a dry-run bootstrap preview for an absent allowed menu", async () => {
|
||||||
|
const { api } = createApi({
|
||||||
|
success: true,
|
||||||
|
operation: "add_footer_menu_item",
|
||||||
|
menuName: "footer-explore",
|
||||||
|
dryRun: true,
|
||||||
|
exists: false,
|
||||||
|
willCreateMenu: true,
|
||||||
|
version: null,
|
||||||
|
item: {
|
||||||
|
label: "Shop",
|
||||||
|
url: "/shop",
|
||||||
|
order: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const result = await handleMenuTool(api, "add_footer_menu_item", {
|
||||||
|
menuName: "footer-explore",
|
||||||
|
label: "Shop",
|
||||||
|
url: "/shop",
|
||||||
|
order: 1,
|
||||||
|
dryRun: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(output(result)).toEqual({
|
||||||
|
success: true,
|
||||||
|
operation: "add_footer_menu_item",
|
||||||
|
menuName: "footer-explore",
|
||||||
|
dryRun: true,
|
||||||
|
exists: false,
|
||||||
|
willCreateMenu: true,
|
||||||
|
version: null,
|
||||||
|
item: {
|
||||||
|
label: "Shop",
|
||||||
|
url: "/shop",
|
||||||
|
order: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test("partially updates an item without forwarding unrelated fields", async () => {
|
test("partially updates an item without forwarding unrelated fields", async () => {
|
||||||
const { api, calls } = createApi();
|
const { api, calls } = createApi();
|
||||||
await handleMenuTool(api, "update_footer_menu_item", {
|
await handleMenuTool(api, "update_footer_menu_item", {
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ const rootMenuNodeSchema = {
|
|||||||
export const MENU_TOOL_DEFINITIONS = [
|
export const MENU_TOOL_DEFINITIONS = [
|
||||||
{
|
{
|
||||||
name: "get_footer_menu",
|
name: "get_footer_menu",
|
||||||
description: "Read footer-explore or footer-support in a compact, language-specific format. Use lang=all only when every translation is required.",
|
description: "Read footer-explore or footer-support in a compact, language-specific format. An allowed menu that does not exist is returned as exists:false, version:null, items:[]. Use lang=all only when every translation is required.",
|
||||||
inputSchema: {
|
inputSchema: {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
@@ -128,7 +128,7 @@ export const MENU_TOOL_DEFINITIONS = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "add_footer_menu_item",
|
name: "add_footer_menu_item",
|
||||||
description: "Add one item to footer-explore or footer-support without replacing the menu. Use dryRun first and pass the version from get_footer_menu.",
|
description: "Add one item to footer-explore or footer-support without replacing the menu. The first actual add creates an allowed footer menu when it does not exist. Use dryRun first and pass the version from get_footer_menu.",
|
||||||
inputSchema: {
|
inputSchema: {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
@@ -435,6 +435,8 @@ function compactFooterResponse(data: any, operation: string, params: any) {
|
|||||||
|
|
||||||
for (const key of [
|
for (const key of [
|
||||||
"dryRun",
|
"dryRun",
|
||||||
|
"exists",
|
||||||
|
"willCreateMenu",
|
||||||
"version",
|
"version",
|
||||||
"lang",
|
"lang",
|
||||||
"item",
|
"item",
|
||||||
|
|||||||
Reference in new issue
Block a user