Skip to content
Open

Dev #32

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
208 changes: 208 additions & 0 deletions bun.lock

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-tooltip": "^1.2.8",
"@tanstack/react-form": "^1.28.0",
"@tanstack/react-query": "^5.90.10",
"better-auth": "1.3.18",
"class-variance-authority": "^0.7.1",
Expand All @@ -36,6 +37,8 @@
"react": "19.2.0",
"react-dom": "19.2.0",
"react-dropzone": "^14.3.8",
"react-markdown": "^10.1.0",
"react-syntax-highlighter": "^16.1.0",
"tailwind-merge": "^3.4.0",
"zustand": "^5.0.8"
},
Expand All @@ -47,6 +50,7 @@
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"@types/react-syntax-highlighter": "^15.5.13",
"babel-plugin-react-compiler": "1.0.0",
"tailwindcss": "^4",
"tw-animate-css": "^1.4.0",
Expand Down
371 changes: 268 additions & 103 deletions server/bun.lock

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"@types/swagger-ui-express": "^4.1.8",
"@types/tmp": "^0.2.6",
"better-auth": "1.3.18",
"canvas": "^3.2.1",
"cors": "^2.8.5",
"dotenv": "^17.2.3",
"drizzle-orm": "^0.44.7",
Expand All @@ -50,7 +51,9 @@
"node-api-analytics": "^1.4.0",
"node-cloudflare-r2": "^0.5.1",
"pdf-parse": "1.1.1",
"pdfjs-dist": "^5.4.530",
"pg": "^8.16.3",
"sharp": "^0.34.5",
"studio": "^0.13.5",
"supertest": "^7.1.4",
"swagger-ui-express": "^5.0.1",
Expand Down
12 changes: 12 additions & 0 deletions server/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import AgentRouter from "./routes/agent.router";
import BinsRouter from "./routes/bins.router";
import DocumentsRouter from "./routes/documents.router";
import ChatsRouter from "./routes/chats.router";
import GlobalChatRouter from "./routes/global-chat.router";
// import UserRouter from "./routes/user.router";
import TrashRouter from "./routes/trash.router";
import { toNodeHandler, fromNodeHeaders } from "better-auth/node";
Expand All @@ -17,6 +18,8 @@ import { env } from "./utils/env";
import { setupSwagger } from "./utils/swagger/swagger";

const app = express();
// Avoid conditional requests (304) for API JSON responses.
app.set("etag", false);
app.use(
cors({
origin: env.FRONTEND_URL,
Expand All @@ -25,6 +28,14 @@ app.use(
}),
);

// Prevent caching of API responses (helps avoid stale UI after mutations).
app.use((req, res, next) => {
if (req.path.startsWith("/api/")) {
res.setHeader("Cache-Control", "no-store");
}
next();
});

// app.use("/api", rateLimiter);
app.all("/api/auth/*splat", toNodeHandler(auth));

Expand All @@ -44,6 +55,7 @@ app.use("/api/agent", AgentRouter);
app.use("/api/bins", requireAuth, BinsRouter);
app.use("/api/documents", requireAuth, DocumentsRouter);
app.use("/api/chats", requireAuth, ChatsRouter);
app.use("/api/global-chat", requireAuth, GlobalChatRouter);
// app.use("/api/users", requireAuth, UserRouter);
app.use("/api/trash", requireAuth, TrashRouter);

Expand Down
13 changes: 5 additions & 8 deletions server/src/bins/bins.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,7 @@ const binsService = new BinsService();
export class BinsController {
async getAllBins(req: Request, res: Response) {
try {
const userId = req.user?.id;
if (!userId) {
return res.status(401).json({ success: false, error: "Unauthorized" });
}
const userId = req.user!.id;

const result = await binsService.getAllBins(userId);
return res.status(200).json(result);
Expand All @@ -28,7 +25,7 @@ export class BinsController {
return res.status(401).json({ success: false, error: "Unauthorized" });
}

const { id } = req.params;
const { id } = req.params as { id: string };
const result = await binsService.getBinById(id, userId);

if (!result.success) {
Expand Down Expand Up @@ -81,10 +78,10 @@ export class BinsController {
return res.status(401).json({ success: false, error: "Unauthorized" });
}

const { id } = req.params;
const { id } = req.params as { id: string };
const { name, description } = req.body;

const result = await binsService.updateBin(id, userId, {
const result = await binsService.updateBin(id as string, userId, {
name,
description,
});
Expand All @@ -109,7 +106,7 @@ export class BinsController {
}
const userId = req.user.id;

const { id } = req.params;
const { id } = req.params as { id: string };
const result = await binsService.deleteBin(id, userId);

if (!result.success) {
Expand Down
34 changes: 34 additions & 0 deletions server/src/bins/bins.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,40 @@ export class BinsRepository {
};
}

/**
* Find a bin by id scoped to user, regardless of deleteFlag.
* Used for trash restore/permanent delete flows.
*/
async findAnyByIdAndUserId(
binId: string,
userId: string,
): Promise<Bin | undefined> {
const result = await db
.select({
id: bins.id,
name: bins.name,
description: bins.description,
deleteFlag: bins.deleteFlag,
deletedAt: bins.deletedAt,
createdAt: bins.createdAt,
updatedAt: bins.updatedAt,
})
.from(bins)
.where(and(eq(bins.id, binId), eq(bins.userId, userId)))
.limit(1);

if (result.length === 0) return undefined;

const bin = result[0];
return {
...bin,
documentCount: await this.getDocumentsCount(bin.id),
createdAt: bin.createdAt.toISOString(),
updatedAt: bin.updatedAt.toISOString(),
deletedAt: bin.deletedAt?.toISOString() || null,
};
}

async create(data: {
userId: string;
name: string;
Expand Down
57 changes: 55 additions & 2 deletions server/src/bins/bins.service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { BinsRepository } from "./bins.repository";
import { DocumentsRepository } from "../documents/documents.repository";
import type { Bin, ApiResponse } from "../types/models";
import { storage } from "../utils/storage";
import { vectorStoreService } from "../langchain/vector-store.service";

export class BinsService {
private binsRepo: BinsRepository;
Expand Down Expand Up @@ -126,7 +128,7 @@ export class BinsService {
}

async restoreBin(binId: string, userId: string): Promise<ApiResponse<Bin>> {
const bin = await this.binsRepo.findById(binId);
const bin = await this.binsRepo.findAnyByIdAndUserId(binId, userId);

if (!bin) {
return {
Expand All @@ -135,6 +137,13 @@ export class BinsService {
};
}

if (!bin.deleteFlag) {
return {
success: false,
error: "Bin is not in trash",
};
}

await this.documentsRepo.restoreByBinId(binId);

const restored = await this.binsRepo.restore(binId);
Expand All @@ -159,8 +168,52 @@ export class BinsService {
binId: string,
userId: string,
): Promise<ApiResponse<void>> {
await this.documentsRepo.permanentDeleteByBinId(binId);
const bin = await this.binsRepo.findAnyByIdAndUserId(binId, userId);

if (!bin) {
return {
success: false,
error: "Bin not found",
};
}

if (!bin.deleteFlag) {
return {
success: false,
error: "Bin is not in trash",
};
}

// Full cleanup: delete each document's storage file + thumbnail + vector chunks (best-effort)
const docs = await this.documentsRepo.findAnyByBinId(binId);
await Promise.allSettled(
docs.map(async (doc) => {
if (doc.filePath) {
try {
await storage.delete(doc.filePath);
} catch (error) {
console.error(`Error deleting doc file ${doc.id}:`, error);
}
}

if (doc.thumbnailPath) {
try {
await storage.delete(doc.thumbnailPath);
} catch (error) {
console.error(`Error deleting doc thumbnail ${doc.id}:`, error);
}
}

try {
await vectorStoreService.deleteByDocumentId(doc.id);
} catch (error) {
console.error(`Error deleting doc chunks ${doc.id}:`, error);
}
}),
);

// Remove document rows and then the bin row
await this.documentsRepo.permanentDeleteByBinId(binId);
const deleted = await this.binsRepo.permanentDelete(binId);

if (!deleted) {
Expand Down
11 changes: 7 additions & 4 deletions server/src/chat/chat.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@ export class ChatController {
return res.status(401).json({ success: false, error: "Unauthorized" });
}

const { chatId } = req.params;
const chatIdParam = req.params.chatId;
const chatId = Array.isArray(chatIdParam) ? chatIdParam[0] : chatIdParam;

const result = await chatService.deleteChat(chatId, userId);

Expand All @@ -109,13 +110,14 @@ export class ChatController {
return res.status(401).json({ success: false, error: "Unauthorized" });
}

const { chatId } = req.params;
const chatIdParam = req.params.chatId;
const chatId = Array.isArray(chatIdParam) ? chatIdParam[0] : chatIdParam;
const { limit } = req.query;

const result = await chatService.getMessagesByChatId(
chatId,
userId,
limit ? parseInt(limit as string) : undefined,
limit ? parseInt(limit as string, 10) : undefined,
);

if (!result.success) {
Expand All @@ -139,7 +141,8 @@ export class ChatController {
return res.status(401).json({ success: false, error: "Unauthorized" });
}

const { chatId } = req.params;
const chatIdParam = req.params.chatId;
const chatId = Array.isArray(chatIdParam) ? chatIdParam[0] : chatIdParam;
const { message, documentIds } = req.body;

if (!message || typeof message !== "string" || message.trim() === "") {
Expand Down
1 change: 1 addition & 0 deletions server/src/db/schemas/documents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export const documents = createTable(
fileType: text("file_type").notNull(),
fileSize: bigint("file_size", { mode: "number" }).notNull(),
filePath: text("file_path").notNull(),
thumbnailPath: text("thumbnail_path"),
content: text("content"),
deleteFlag: boolean("delete_flag").default(false).notNull(),
deletedAt: timestamp("deleted_at", { withTimezone: true }),
Expand Down
Loading
Loading