Files
calchat/apps/server/src/app.ts
Linus Waldowsky 2b999d9b0f feat: add recurring event deletion with three modes
Implement three deletion modes for recurring events:
- single: exclude specific occurrence via EXDATE mechanism
- future: set RRULE UNTIL to stop future occurrences
- all: delete entire event series

Changes include:
- Add exceptionDates field to CalendarEvent model
- Add RecurringDeleteMode type and DeleteRecurringEventDTO
- EventService.deleteRecurring() with mode-based logic using rrule library
- EventController DELETE endpoint accepts mode/occurrenceDate query params
- recurrenceExpander filters out exception dates during expansion
- AI tools support deleteMode and occurrenceDate for proposed deletions
- ChatService.confirmEvent() handles recurring delete modes
- New DeleteEventModal component for unified delete confirmation UI
- Calendar screen integrates modal for both recurring and non-recurring events
2026-01-25 15:19:31 +01:00

127 lines
2.9 KiB
TypeScript

import express from "express";
import mongoose from "mongoose";
import "dotenv/config";
import { createRoutes } from "./routes";
import {
AuthController,
ChatController,
EventController,
httpLogger,
} from "./controllers";
import { AuthService, ChatService, EventService } from "./services";
import {
MongoUserRepository,
MongoEventRepository,
MongoChatRepository,
} from "./repositories";
import { GPTAdapter } from "./ai";
import { logger } from "./logging";
const app = express();
const port = process.env.PORT || 3000;
const mongoUri = process.env.MONGODB_URI || "mongodb://localhost:27017/calchat";
// Middleware
app.use(express.json());
app.use(httpLogger);
// CORS - only needed for web browser development
// Native mobile apps don't send Origin headers and aren't affected by CORS
if (process.env.NODE_ENV !== "production") {
app.use((req, res, next) => {
res.header("Access-Control-Allow-Origin", "*");
res.header(
"Access-Control-Allow-Methods",
"GET, POST, PUT, DELETE, OPTIONS",
);
res.header(
"Access-Control-Allow-Headers",
"Content-Type, Authorization, X-User-Id",
);
if (req.method === "OPTIONS") {
res.sendStatus(200);
return;
}
next();
});
}
// Initialize repositories
const userRepo = new MongoUserRepository();
const eventRepo = new MongoEventRepository();
const chatRepo = new MongoChatRepository();
// Initialize AI provider
const aiProvider = new GPTAdapter();
// Initialize services
const authService = new AuthService(userRepo);
const eventService = new EventService(eventRepo);
const chatService = new ChatService(
chatRepo,
eventRepo,
eventService,
aiProvider,
);
// Initialize controllers
const authController = new AuthController(authService);
const chatController = new ChatController(chatService);
const eventController = new EventController(eventService);
// Setup routes
app.use(
"/api",
createRoutes({
authController,
chatController,
eventController,
}),
);
// Health check
app.get("/health", (_, res) => {
res.json({ status: "ok" });
});
// AI Test endpoint (for development only)
app.post("/api/ai/test", async (req, res) => {
try {
const { message } = req.body;
if (!message) {
res.status(400).json({ error: "message is required" });
return;
}
const result = await aiProvider.processMessage(message, {
userId: "test-user",
conversationHistory: [],
existingEvents: [],
currentDate: new Date(),
});
res.json(result);
} catch (error) {
logger.error({ error }, "AI test error");
res.status(500).json({ error: String(error) });
}
});
// Start server
async function start() {
try {
await mongoose.connect(mongoUri);
logger.info("Connected to MongoDB");
app.listen(port, () => {
logger.info({ port }, "Server started");
});
} catch (error) {
logger.fatal({ error }, "Failed to start server");
process.exit(1);
}
}
start();
export default app;