chore(25-02): apply biome formatter to task 1 and 2 files
This commit is contained in:
@@ -67,15 +67,15 @@ const GlobalItemsGlobalItemIdRoute = GlobalItemsGlobalItemIdRouteImport.update({
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const ThreadsThreadIdIndexRoute = ThreadsThreadIdIndexRouteImport.update({
|
||||
id: '/',
|
||||
path: '/',
|
||||
getParentRoute: () => ThreadsThreadIdRoute,
|
||||
id: '/threads/$threadId/',
|
||||
path: '/threads/$threadId/',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const ThreadsThreadIdCandidatesCandidateIdRoute =
|
||||
ThreadsThreadIdCandidatesCandidateIdRouteImport.update({
|
||||
id: '/candidates/$candidateId',
|
||||
path: '/candidates/$candidateId',
|
||||
getParentRoute: () => ThreadsThreadIdRoute,
|
||||
id: '/threads/$threadId/candidates/$candidateId',
|
||||
path: '/threads/$threadId/candidates/$candidateId',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
@@ -170,6 +170,8 @@ export interface RootRouteChildren {
|
||||
UsersUserIdRoute: typeof UsersUserIdRoute
|
||||
CollectionIndexRoute: typeof CollectionIndexRoute
|
||||
GlobalItemsIndexRoute: typeof GlobalItemsIndexRoute
|
||||
ThreadsThreadIdIndexRoute: typeof ThreadsThreadIdIndexRoute
|
||||
ThreadsThreadIdCandidatesCandidateIdRoute: typeof ThreadsThreadIdCandidatesCandidateIdRoute
|
||||
}
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
@@ -239,17 +241,17 @@ declare module '@tanstack/react-router' {
|
||||
}
|
||||
'/threads/$threadId/': {
|
||||
id: '/threads/$threadId/'
|
||||
path: '/'
|
||||
path: '/threads/$threadId'
|
||||
fullPath: '/threads/$threadId/'
|
||||
preLoaderRoute: typeof ThreadsThreadIdIndexRouteImport
|
||||
parentRoute: typeof ThreadsThreadIdRoute
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/threads/$threadId/candidates/$candidateId': {
|
||||
id: '/threads/$threadId/candidates/$candidateId'
|
||||
path: '/candidates/$candidateId'
|
||||
path: '/threads/$threadId/candidates/$candidateId'
|
||||
fullPath: '/threads/$threadId/candidates/$candidateId'
|
||||
preLoaderRoute: typeof ThreadsThreadIdCandidatesCandidateIdRouteImport
|
||||
parentRoute: typeof ThreadsThreadIdRoute
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -264,6 +266,9 @@ const rootRouteChildren: RootRouteChildren = {
|
||||
UsersUserIdRoute: UsersUserIdRoute,
|
||||
CollectionIndexRoute: CollectionIndexRoute,
|
||||
GlobalItemsIndexRoute: GlobalItemsIndexRoute,
|
||||
ThreadsThreadIdIndexRoute: ThreadsThreadIdIndexRoute,
|
||||
ThreadsThreadIdCandidatesCandidateIdRoute:
|
||||
ThreadsThreadIdCandidatesCandidateIdRoute,
|
||||
}
|
||||
export const routeTree = rootRouteImport
|
||||
._addFileChildren(rootRouteChildren)
|
||||
|
||||
@@ -16,21 +16,43 @@ function textResult(data: unknown): ToolResult {
|
||||
}
|
||||
|
||||
function errorResult(message: string): ToolResult {
|
||||
return { content: [{ type: "text", text: JSON.stringify({ error: message }) }] };
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify({ error: message }) }],
|
||||
};
|
||||
}
|
||||
|
||||
const catalogItemInputSchema = {
|
||||
brand: z.string().describe("Brand or manufacturer name"),
|
||||
model: z.string().describe("Model name — combined with brand forms the unique identifier"),
|
||||
category: z.string().optional().describe("Category name (e.g., 'Bags', 'Lights')"),
|
||||
model: z
|
||||
.string()
|
||||
.describe("Model name — combined with brand forms the unique identifier"),
|
||||
category: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Category name (e.g., 'Bags', 'Lights')"),
|
||||
weightGrams: z.number().optional().describe("Weight in grams"),
|
||||
priceCents: z.number().optional().describe("MSRP price in cents (e.g., 9999 = $99.99)"),
|
||||
priceCents: z
|
||||
.number()
|
||||
.optional()
|
||||
.describe("MSRP price in cents (e.g., 9999 = $99.99)"),
|
||||
imageUrl: z.string().optional().describe("URL to the product image"),
|
||||
description: z.string().optional().describe("Product description"),
|
||||
sourceUrl: z.string().optional().describe("URL to the product page on manufacturer/retailer site"),
|
||||
imageCredit: z.string().optional().describe("Image credit — photographer or source name"),
|
||||
imageSourceUrl: z.string().optional().describe("Original URL where the image was sourced from"),
|
||||
tags: z.array(z.string()).optional().describe("Tags for categorization (created automatically if new)"),
|
||||
sourceUrl: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("URL to the product page on manufacturer/retailer site"),
|
||||
imageCredit: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Image credit — photographer or source name"),
|
||||
imageSourceUrl: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Original URL where the image was sourced from"),
|
||||
tags: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe("Tags for categorization (created automatically if new)"),
|
||||
};
|
||||
|
||||
export const catalogToolDefinitions = [
|
||||
|
||||
@@ -48,11 +48,15 @@ app.post("/", zValidator("json", upsertGlobalItemSchema), async (c) => {
|
||||
});
|
||||
|
||||
// Bulk upsert — per D-06, D-07, D-08
|
||||
app.post("/bulk", zValidator("json", bulkUpsertGlobalItemsSchema), async (c) => {
|
||||
const db = c.get("db");
|
||||
const { items } = c.req.valid("json");
|
||||
const result = await bulkUpsertGlobalItems(db, items);
|
||||
return c.json(result);
|
||||
});
|
||||
app.post(
|
||||
"/bulk",
|
||||
zValidator("json", bulkUpsertGlobalItemsSchema),
|
||||
async (c) => {
|
||||
const db = c.get("db");
|
||||
const { items } = c.req.valid("json");
|
||||
const result = await bulkUpsertGlobalItems(db, items);
|
||||
return c.json(result);
|
||||
},
|
||||
);
|
||||
|
||||
export { app as globalItemRoutes };
|
||||
|
||||
@@ -302,12 +302,17 @@ describe("MCP Catalog Tools", () => {
|
||||
model: "PocketRocket 2",
|
||||
sourceUrl: "https://www.cascadedesigns.com/msr/pocket-rocket-2",
|
||||
imageCredit: "MSR Photography",
|
||||
imageSourceUrl: "https://cdn.cascadedesigns.com/images/pocket-rocket-2.jpg",
|
||||
imageSourceUrl:
|
||||
"https://cdn.cascadedesigns.com/images/pocket-rocket-2.jpg",
|
||||
});
|
||||
const data = parseResult(result);
|
||||
expect(data.sourceUrl).toBe("https://www.cascadedesigns.com/msr/pocket-rocket-2");
|
||||
expect(data.sourceUrl).toBe(
|
||||
"https://www.cascadedesigns.com/msr/pocket-rocket-2",
|
||||
);
|
||||
expect(data.imageCredit).toBe("MSR Photography");
|
||||
expect(data.imageSourceUrl).toBe("https://cdn.cascadedesigns.com/images/pocket-rocket-2.jpg");
|
||||
expect(data.imageSourceUrl).toBe(
|
||||
"https://cdn.cascadedesigns.com/images/pocket-rocket-2.jpg",
|
||||
);
|
||||
});
|
||||
|
||||
test("bulk_upsert_catalog processes array and returns created/updated counts", async () => {
|
||||
@@ -333,7 +338,10 @@ describe("MCP Catalog Tools", () => {
|
||||
const tools = registerCatalogTools(db);
|
||||
|
||||
// Pre-create one item
|
||||
await tools.upsert_catalog_item({ brand: "Revelate Designs", model: "Terrapin System" });
|
||||
await tools.upsert_catalog_item({
|
||||
brand: "Revelate Designs",
|
||||
model: "Terrapin System",
|
||||
});
|
||||
|
||||
const result = await tools.bulk_upsert_catalog({
|
||||
items: [
|
||||
@@ -348,7 +356,9 @@ describe("MCP Catalog Tools", () => {
|
||||
});
|
||||
|
||||
test("catalog tool definitions include attribution fields in inputSchema", () => {
|
||||
const { catalogToolDefinitions } = require("../../src/server/mcp/tools/catalog.ts");
|
||||
const {
|
||||
catalogToolDefinitions,
|
||||
} = require("../../src/server/mcp/tools/catalog.ts");
|
||||
const upsertDef = catalogToolDefinitions.find(
|
||||
(d: { name: string }) => d.name === "upsert_catalog_item",
|
||||
);
|
||||
|
||||
@@ -112,136 +112,139 @@ describe("Global Item Routes", () => {
|
||||
});
|
||||
|
||||
describe("POST /api/global-items", () => {
|
||||
it("returns 200 with item and created=true on new item", async () => {
|
||||
const res = await app.request("/api/global-items", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ brand: "Revelate Designs", model: "Terrapin System" }),
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
it("returns 200 with item and created=true on new item", async () => {
|
||||
const res = await app.request("/api/global-items", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
brand: "Revelate Designs",
|
||||
model: "Terrapin System",
|
||||
}),
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
const body = await res.json();
|
||||
expect(body.item.brand).toBe("Revelate Designs");
|
||||
expect(body.item.model).toBe("Terrapin System");
|
||||
expect(body.created).toBe(true);
|
||||
const body = await res.json();
|
||||
expect(body.item.brand).toBe("Revelate Designs");
|
||||
expect(body.item.model).toBe("Terrapin System");
|
||||
expect(body.created).toBe(true);
|
||||
});
|
||||
|
||||
it("returns 200 with created=false when upserting existing item", async () => {
|
||||
await insertGlobalItem(db, "Revelate Designs", "Terrapin System");
|
||||
|
||||
const res = await app.request("/api/global-items", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
brand: "Revelate Designs",
|
||||
model: "Terrapin System",
|
||||
description: "Updated description",
|
||||
}),
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
const body = await res.json();
|
||||
expect(body.created).toBe(false);
|
||||
expect(body.item.description).toBe("Updated description");
|
||||
});
|
||||
|
||||
it("returns 400 when brand is missing", async () => {
|
||||
const res = await app.request("/api/global-items", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ model: "Terrapin System" }),
|
||||
});
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
|
||||
it("returns 400 when model is missing", async () => {
|
||||
const res = await app.request("/api/global-items", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ brand: "Revelate Designs" }),
|
||||
});
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
});
|
||||
|
||||
it("returns 200 with created=false when upserting existing item", async () => {
|
||||
await insertGlobalItem(db, "Revelate Designs", "Terrapin System");
|
||||
describe("POST /api/global-items/bulk", () => {
|
||||
it("returns 200 with created/updated counts", async () => {
|
||||
const res = await app.request("/api/global-items/bulk", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
items: [
|
||||
{ brand: "Revelate Designs", model: "Terrapin System" },
|
||||
{ brand: "Apidura", model: "Handlebar Pack" },
|
||||
],
|
||||
}),
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
const res = await app.request("/api/global-items", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
brand: "Revelate Designs",
|
||||
model: "Terrapin System",
|
||||
description: "Updated description",
|
||||
}),
|
||||
const body = await res.json();
|
||||
expect(body.created).toBe(2);
|
||||
expect(body.updated).toBe(0);
|
||||
expect(body.items).toHaveLength(2);
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
const body = await res.json();
|
||||
expect(body.created).toBe(false);
|
||||
expect(body.item.description).toBe("Updated description");
|
||||
it("returns correct counts for mix of new and existing items", async () => {
|
||||
await insertGlobalItem(db, "Revelate Designs", "Terrapin System");
|
||||
|
||||
const res = await app.request("/api/global-items/bulk", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
items: [
|
||||
{ brand: "Revelate Designs", model: "Terrapin System" },
|
||||
{ brand: "Apidura", model: "Handlebar Pack" },
|
||||
],
|
||||
}),
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
const body = await res.json();
|
||||
expect(body.created).toBe(1);
|
||||
expect(body.updated).toBe(1);
|
||||
});
|
||||
|
||||
it("returns 400 when items array is empty", async () => {
|
||||
const res = await app.request("/api/global-items/bulk", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ items: [] }),
|
||||
});
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
|
||||
it("returns 400 when items array exceeds 100", async () => {
|
||||
const items = Array.from({ length: 101 }, (_, i) => ({
|
||||
brand: `Brand${i}`,
|
||||
model: `Model${i}`,
|
||||
}));
|
||||
const res = await app.request("/api/global-items/bulk", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ items }),
|
||||
});
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
|
||||
it("returns 400 for invalid item in array (missing brand)", async () => {
|
||||
const res = await app.request("/api/global-items/bulk", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
items: [
|
||||
{ brand: "Revelate Designs", model: "Terrapin System" },
|
||||
{ model: "Invalid Item without brand" },
|
||||
],
|
||||
}),
|
||||
});
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
});
|
||||
|
||||
it("returns 400 when brand is missing", async () => {
|
||||
const res = await app.request("/api/global-items", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ model: "Terrapin System" }),
|
||||
});
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
|
||||
it("returns 400 when model is missing", async () => {
|
||||
const res = await app.request("/api/global-items", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ brand: "Revelate Designs" }),
|
||||
});
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
});
|
||||
|
||||
describe("POST /api/global-items/bulk", () => {
|
||||
it("returns 200 with created/updated counts", async () => {
|
||||
const res = await app.request("/api/global-items/bulk", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
items: [
|
||||
{ brand: "Revelate Designs", model: "Terrapin System" },
|
||||
{ brand: "Apidura", model: "Handlebar Pack" },
|
||||
],
|
||||
}),
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
const body = await res.json();
|
||||
expect(body.created).toBe(2);
|
||||
expect(body.updated).toBe(0);
|
||||
expect(body.items).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("returns correct counts for mix of new and existing items", async () => {
|
||||
await insertGlobalItem(db, "Revelate Designs", "Terrapin System");
|
||||
|
||||
const res = await app.request("/api/global-items/bulk", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
items: [
|
||||
{ brand: "Revelate Designs", model: "Terrapin System" },
|
||||
{ brand: "Apidura", model: "Handlebar Pack" },
|
||||
],
|
||||
}),
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
const body = await res.json();
|
||||
expect(body.created).toBe(1);
|
||||
expect(body.updated).toBe(1);
|
||||
});
|
||||
|
||||
it("returns 400 when items array is empty", async () => {
|
||||
const res = await app.request("/api/global-items/bulk", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ items: [] }),
|
||||
});
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
|
||||
it("returns 400 when items array exceeds 100", async () => {
|
||||
const items = Array.from({ length: 101 }, (_, i) => ({
|
||||
brand: `Brand${i}`,
|
||||
model: `Model${i}`,
|
||||
}));
|
||||
const res = await app.request("/api/global-items/bulk", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ items }),
|
||||
});
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
|
||||
it("returns 400 for invalid item in array (missing brand)", async () => {
|
||||
const res = await app.request("/api/global-items/bulk", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
items: [
|
||||
{ brand: "Revelate Designs", model: "Terrapin System" },
|
||||
{ model: "Invalid Item without brand" },
|
||||
],
|
||||
}),
|
||||
});
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
});
|
||||
|
||||
describe("GET /api/global-items/:id", () => {
|
||||
describe("GET /api/global-items/:id", () => {
|
||||
it("returns item with ownerCount", async () => {
|
||||
const gi = await insertGlobalItem(db, "MSR", "PocketRocket 2");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user