feat(17-02): wire storage service into all routes and MCP tools, remove static /uploads/*
- Replace unlink() with deleteImage() in items and threads routes - Add withImageUrl/withImageUrls to item, thread, setup GET responses - Enrich MCP tool responses with presigned image URLs - Remove /uploads/* static file serving from server index - Update MCP image tool description (local -> storage)
This commit is contained in:
@@ -92,9 +92,6 @@ if (process.env.GEARBOX_MCP !== "false") {
|
|||||||
app.route("/mcp", mcpRoutes);
|
app.route("/mcp", mcpRoutes);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Serve uploaded images
|
|
||||||
app.use("/uploads/*", serveStatic({ root: "./" }));
|
|
||||||
|
|
||||||
// Serve Vite-built SPA in production
|
// Serve Vite-built SPA in production
|
||||||
if (process.env.NODE_ENV === "production") {
|
if (process.env.NODE_ENV === "production") {
|
||||||
app.use("/*", serveStatic({ root: "./dist/client" }));
|
app.use("/*", serveStatic({ root: "./dist/client" }));
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export const imageToolDefinitions = [
|
|||||||
{
|
{
|
||||||
name: "upload_image_from_url",
|
name: "upload_image_from_url",
|
||||||
description:
|
description:
|
||||||
"Fetch an image from a URL and save it locally. Returns the filename to use with create_item or add_candidate.",
|
"Fetch an image from a URL and upload it to storage. Returns the filename to use with create_item or add_candidate.",
|
||||||
inputSchema: {
|
inputSchema: {
|
||||||
url: z
|
url: z
|
||||||
.string()
|
.string()
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
getItemById,
|
getItemById,
|
||||||
updateItem,
|
updateItem,
|
||||||
} from "../../services/item.service.ts";
|
} from "../../services/item.service.ts";
|
||||||
|
import { withImageUrl, withImageUrls } from "../../services/storage.service.ts";
|
||||||
|
|
||||||
type Db = typeof prodDb;
|
type Db = typeof prodDb;
|
||||||
|
|
||||||
@@ -95,13 +96,11 @@ export function registerItemTools(db: Db, userId: number) {
|
|||||||
return {
|
return {
|
||||||
list_items: async (args: { categoryId?: number }): Promise<ToolResult> => {
|
list_items: async (args: { categoryId?: number }): Promise<ToolResult> => {
|
||||||
try {
|
try {
|
||||||
const items = await getAllItems(db, userId);
|
let items = await getAllItems(db, userId);
|
||||||
if (args.categoryId) {
|
if (args.categoryId) {
|
||||||
return textResult(
|
items = items.filter((i) => i.categoryId === args.categoryId);
|
||||||
items.filter((i) => i.categoryId === args.categoryId),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return textResult(items);
|
return textResult(await withImageUrls(items));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return errorResult((err as Error).message);
|
return errorResult((err as Error).message);
|
||||||
}
|
}
|
||||||
@@ -111,7 +110,7 @@ export function registerItemTools(db: Db, userId: number) {
|
|||||||
try {
|
try {
|
||||||
const item = await getItemById(db, userId, args.id);
|
const item = await getItemById(db, userId, args.id);
|
||||||
if (!item) return errorResult(`Item ${args.id} not found`);
|
if (!item) return errorResult(`Item ${args.id} not found`);
|
||||||
return textResult(item);
|
return textResult(await withImageUrl(item));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return errorResult((err as Error).message);
|
return errorResult((err as Error).message);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import type { db as prodDb } from "../../../db/index.ts";
|
import type { db as prodDb } from "../../../db/index.ts";
|
||||||
|
import { withImageUrls } from "../../services/storage.service.ts";
|
||||||
import {
|
import {
|
||||||
createCandidate,
|
createCandidate,
|
||||||
createThread,
|
createThread,
|
||||||
@@ -134,7 +135,8 @@ export function registerThreadTools(db: Db, userId: number) {
|
|||||||
try {
|
try {
|
||||||
const thread = await getThreadWithCandidates(db, userId, args.id);
|
const thread = await getThreadWithCandidates(db, userId, args.id);
|
||||||
if (!thread) return errorResult(`Thread ${args.id} not found`);
|
if (!thread) return errorResult(`Thread ${args.id} not found`);
|
||||||
return textResult(thread);
|
const enrichedCandidates = await withImageUrls(thread.candidates);
|
||||||
|
return textResult({ ...thread, candidates: enrichedCandidates });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return errorResult((err as Error).message);
|
return errorResult((err as Error).message);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import { unlink } from "node:fs/promises";
|
|
||||||
import { join } from "node:path";
|
|
||||||
import { zValidator } from "@hono/zod-validator";
|
import { zValidator } from "@hono/zod-validator";
|
||||||
import { Hono } from "hono";
|
import { Hono } from "hono";
|
||||||
import { createItemSchema, updateItemSchema } from "../../shared/schemas.ts";
|
import { createItemSchema, updateItemSchema } from "../../shared/schemas.ts";
|
||||||
@@ -13,6 +11,11 @@ import {
|
|||||||
getItemById,
|
getItemById,
|
||||||
updateItem,
|
updateItem,
|
||||||
} from "../services/item.service.ts";
|
} from "../services/item.service.ts";
|
||||||
|
import {
|
||||||
|
deleteImage,
|
||||||
|
withImageUrl,
|
||||||
|
withImageUrls,
|
||||||
|
} from "../services/storage.service.ts";
|
||||||
|
|
||||||
type Env = { Variables: { db?: any; userId?: number } };
|
type Env = { Variables: { db?: any; userId?: number } };
|
||||||
|
|
||||||
@@ -45,7 +48,7 @@ app.get("/", async (c) => {
|
|||||||
const db = c.get("db");
|
const db = c.get("db");
|
||||||
const userId = c.get("userId")!;
|
const userId = c.get("userId")!;
|
||||||
const items = await getAllItems(db, userId);
|
const items = await getAllItems(db, userId);
|
||||||
return c.json(items);
|
return c.json(await withImageUrls(items));
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get("/:id", async (c) => {
|
app.get("/:id", async (c) => {
|
||||||
@@ -55,7 +58,7 @@ app.get("/:id", async (c) => {
|
|||||||
if (!id) return c.json({ error: "Invalid item ID" }, 400);
|
if (!id) return c.json({ error: "Invalid item ID" }, 400);
|
||||||
const item = await getItemById(db, userId, id);
|
const item = await getItemById(db, userId, id);
|
||||||
if (!item) return c.json({ error: "Item not found" }, 404);
|
if (!item) return c.json({ error: "Item not found" }, 404);
|
||||||
return c.json(item);
|
return c.json(await withImageUrl(item));
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post("/", zValidator("json", createItemSchema), async (c) => {
|
app.post("/", zValidator("json", createItemSchema), async (c) => {
|
||||||
@@ -99,12 +102,12 @@ app.delete("/:id", async (c) => {
|
|||||||
const deleted = await deleteItem(db, userId, id);
|
const deleted = await deleteItem(db, userId, id);
|
||||||
if (!deleted) return c.json({ error: "Item not found" }, 404);
|
if (!deleted) return c.json({ error: "Item not found" }, 404);
|
||||||
|
|
||||||
// Clean up image file if exists
|
// Clean up image from object storage if exists
|
||||||
if (deleted.imageFilename) {
|
if (deleted.imageFilename) {
|
||||||
try {
|
try {
|
||||||
await unlink(join("uploads", deleted.imageFilename));
|
await deleteImage(deleted.imageFilename);
|
||||||
} catch {
|
} catch {
|
||||||
// File missing is not an error worth failing the delete over
|
// Missing object is not an error worth failing the delete over
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
updateSetupSchema,
|
updateSetupSchema,
|
||||||
} from "../../shared/schemas.ts";
|
} from "../../shared/schemas.ts";
|
||||||
import { parseId } from "../lib/params.ts";
|
import { parseId } from "../lib/params.ts";
|
||||||
|
import { withImageUrls } from "../services/storage.service.ts";
|
||||||
import {
|
import {
|
||||||
createSetup,
|
createSetup,
|
||||||
deleteSetup,
|
deleteSetup,
|
||||||
@@ -46,7 +47,8 @@ app.get("/:id", async (c) => {
|
|||||||
if (!id) return c.json({ error: "Invalid setup ID" }, 400);
|
if (!id) return c.json({ error: "Invalid setup ID" }, 400);
|
||||||
const setup = await getSetupWithItems(db, userId, id);
|
const setup = await getSetupWithItems(db, userId, id);
|
||||||
if (!setup) return c.json({ error: "Setup not found" }, 404);
|
if (!setup) return c.json({ error: "Setup not found" }, 404);
|
||||||
return c.json(setup);
|
const enrichedItems = await withImageUrls(setup.items);
|
||||||
|
return c.json({ ...setup, items: enrichedItems });
|
||||||
});
|
});
|
||||||
|
|
||||||
app.put("/:id", zValidator("json", updateSetupSchema), async (c) => {
|
app.put("/:id", zValidator("json", updateSetupSchema), async (c) => {
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import { unlink } from "node:fs/promises";
|
|
||||||
import { join } from "node:path";
|
|
||||||
import { zValidator } from "@hono/zod-validator";
|
import { zValidator } from "@hono/zod-validator";
|
||||||
import { Hono } from "hono";
|
import { Hono } from "hono";
|
||||||
import {
|
import {
|
||||||
@@ -11,6 +9,10 @@ import {
|
|||||||
updateThreadSchema,
|
updateThreadSchema,
|
||||||
} from "../../shared/schemas.ts";
|
} from "../../shared/schemas.ts";
|
||||||
import { parseId } from "../lib/params.ts";
|
import { parseId } from "../lib/params.ts";
|
||||||
|
import {
|
||||||
|
deleteImage,
|
||||||
|
withImageUrls,
|
||||||
|
} from "../services/storage.service.ts";
|
||||||
import {
|
import {
|
||||||
createCandidate,
|
createCandidate,
|
||||||
createThread,
|
createThread,
|
||||||
@@ -53,7 +55,8 @@ app.get("/:id", async (c) => {
|
|||||||
if (!id) return c.json({ error: "Invalid thread ID" }, 400);
|
if (!id) return c.json({ error: "Invalid thread ID" }, 400);
|
||||||
const thread = await getThreadWithCandidates(db, userId, id);
|
const thread = await getThreadWithCandidates(db, userId, id);
|
||||||
if (!thread) return c.json({ error: "Thread not found" }, 404);
|
if (!thread) return c.json({ error: "Thread not found" }, 404);
|
||||||
return c.json(thread);
|
const enrichedCandidates = await withImageUrls(thread.candidates);
|
||||||
|
return c.json({ ...thread, candidates: enrichedCandidates });
|
||||||
});
|
});
|
||||||
|
|
||||||
app.put("/:id", zValidator("json", updateThreadSchema), async (c) => {
|
app.put("/:id", zValidator("json", updateThreadSchema), async (c) => {
|
||||||
@@ -75,12 +78,12 @@ app.delete("/:id", async (c) => {
|
|||||||
const deleted = await deleteThread(db, userId, id);
|
const deleted = await deleteThread(db, userId, id);
|
||||||
if (!deleted) return c.json({ error: "Thread not found" }, 404);
|
if (!deleted) return c.json({ error: "Thread not found" }, 404);
|
||||||
|
|
||||||
// Clean up candidate image files
|
// Clean up candidate images from object storage
|
||||||
for (const filename of deleted.candidateImages) {
|
for (const filename of deleted.candidateImages) {
|
||||||
try {
|
try {
|
||||||
await unlink(join("uploads", filename));
|
await deleteImage(filename);
|
||||||
} catch {
|
} catch {
|
||||||
// File missing is not an error worth failing the delete over
|
// Missing object is not an error worth failing the delete over
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,12 +134,12 @@ app.delete("/:threadId/candidates/:candidateId", async (c) => {
|
|||||||
const deleted = await deleteCandidate(db, userId, candidateId);
|
const deleted = await deleteCandidate(db, userId, candidateId);
|
||||||
if (!deleted) return c.json({ error: "Candidate not found" }, 404);
|
if (!deleted) return c.json({ error: "Candidate not found" }, 404);
|
||||||
|
|
||||||
// Clean up image file if exists
|
// Clean up image from object storage if exists
|
||||||
if (deleted.imageFilename) {
|
if (deleted.imageFilename) {
|
||||||
try {
|
try {
|
||||||
await unlink(join("uploads", deleted.imageFilename));
|
await deleteImage(deleted.imageFilename);
|
||||||
} catch {
|
} catch {
|
||||||
// File missing is not an error
|
// Missing object is not an error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user