format codebase with prettier

This commit is contained in:
2026-01-04 16:17:36 +01:00
parent 77f15b6dd1
commit e3f7a778c7
63 changed files with 786 additions and 542 deletions

View File

@@ -1,18 +1,21 @@
import Anthropic from '@anthropic-ai/sdk';
import { AIProvider, AIContext, AIResponse } from '../services/interfaces';
import Anthropic from "@anthropic-ai/sdk";
import { AIProvider, AIContext, AIResponse } from "../services/interfaces";
export class ClaudeAdapter implements AIProvider {
private client: Anthropic;
private model: string;
constructor(apiKey?: string, model: string = 'claude-3-haiku-20240307') {
constructor(apiKey?: string, model: string = "claude-3-haiku-20240307") {
this.client = new Anthropic({
apiKey: apiKey || process.env.ANTHROPIC_API_KEY,
});
this.model = model;
}
async processMessage(message: string, context: AIContext): Promise<AIResponse> {
throw new Error('Not implemented');
async processMessage(
message: string,
context: AIContext,
): Promise<AIResponse> {
throw new Error("Not implemented");
}
}

View File

@@ -1 +1 @@
export * from './ClaudeAdapter';
export * from "./ClaudeAdapter";

View File

@@ -1,28 +1,35 @@
import express from 'express';
import mongoose from 'mongoose';
import 'dotenv/config'
import express from "express";
import mongoose from "mongoose";
import "dotenv/config";
import { createRoutes } from './routes';
import { AuthController, ChatController, EventController } from './controllers';
import { AuthService, ChatService, EventService } from './services';
import { MongoUserRepository, MongoEventRepository, MongoChatRepository } from './repositories';
import { ClaudeAdapter } from './ai';
import { createRoutes } from "./routes";
import { AuthController, ChatController, EventController } from "./controllers";
import { AuthService, ChatService, EventService } from "./services";
import {
MongoUserRepository,
MongoEventRepository,
MongoChatRepository,
} from "./repositories";
import { ClaudeAdapter } from "./ai";
const app = express();
const port = process.env.PORT || 3000;
const mongoUri = process.env.MONGODB_URI || 'mongodb://localhost:27017/caldav';
const mongoUri = process.env.MONGODB_URI || "mongodb://localhost:27017/caldav";
// Middleware
app.use(express.json());
// 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') {
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');
if (req.method === 'OPTIONS') {
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");
if (req.method === "OPTIONS") {
res.sendStatus(200);
return;
}
@@ -49,34 +56,37 @@ const chatController = new ChatController(chatService);
const eventController = new EventController(eventService);
// Setup routes
app.use('/api', createRoutes({
authController,
chatController,
eventController,
}));
app.use(
"/api",
createRoutes({
authController,
chatController,
eventController,
}),
);
// Health check
app.get('/health', (_, res) => {
res.json({ status: 'ok' });
app.get("/health", (_, res) => {
res.json({ status: "ok" });
});
// AI Test endpoint (for development only)
app.post('/api/ai/test', async (req, res) => {
app.post("/api/ai/test", async (req, res) => {
try {
const { message } = req.body;
if (!message) {
res.status(400).json({ error: 'message is required' });
res.status(400).json({ error: "message is required" });
return;
}
const result = await aiProvider.processMessage(message, {
userId: 'test-user',
userId: "test-user",
conversationHistory: [],
existingEvents: [],
currentDate: new Date(),
});
res.json(result);
} catch (error) {
console.error('AI test error:', error);
console.error("AI test error:", error);
res.status(500).json({ error: String(error) });
}
});
@@ -85,13 +95,13 @@ app.post('/api/ai/test', async (req, res) => {
async function start() {
try {
await mongoose.connect(mongoUri);
console.log('Connected to MongoDB');
console.log("Connected to MongoDB");
app.listen(port, () => {
console.log(`Server running on port ${port}`);
});
} catch (error) {
console.error('Failed to start server:', error);
console.error("Failed to start server:", error);
process.exit(1);
}
}

View File

@@ -1,5 +1,5 @@
import { Request, Response } from 'express';
import { AuthService } from '../services';
import { Request, Response } from "express";
import { AuthService } from "../services";
export class AuthController {
constructor(private authService: AuthService) {}
@@ -23,10 +23,10 @@ export class AuthController {
}
async refresh(req: Request, res: Response): Promise<void> {
throw new Error('Not implemented');
throw new Error("Not implemented");
}
async logout(req: Request, res: Response): Promise<void> {
throw new Error('Not implemented');
throw new Error("Not implemented");
}
}

View File

@@ -1,7 +1,12 @@
import { Response } from 'express';
import { SendMessageDTO, CreateEventDTO, UpdateEventDTO, EventAction } from '@caldav/shared';
import { ChatService } from '../services';
import { AuthenticatedRequest } from '../middleware';
import { Response } from "express";
import {
SendMessageDTO,
CreateEventDTO,
UpdateEventDTO,
EventAction,
} from "@caldav/shared";
import { ChatService } from "../services";
import { AuthenticatedRequest } from "../middleware";
export class ChatController {
constructor(private chatService: ChatService) {}
@@ -13,7 +18,7 @@ export class ChatController {
const response = await this.chatService.processMessage(userId, data);
res.json(response);
} catch (error) {
res.status(500).json({ error: 'Failed to process message' });
res.status(500).json({ error: "Failed to process message" });
}
}
@@ -34,11 +39,11 @@ export class ChatController {
action,
event,
eventId,
updates
updates,
);
res.json(response);
} catch (error) {
res.status(500).json({ error: 'Failed to confirm event' });
res.status(500).json({ error: "Failed to confirm event" });
}
}
@@ -46,18 +51,28 @@ export class ChatController {
try {
const userId = req.user!.userId;
const { conversationId, messageId } = req.params;
const response = await this.chatService.rejectEvent(userId, conversationId, messageId);
const response = await this.chatService.rejectEvent(
userId,
conversationId,
messageId,
);
res.json(response);
} catch (error) {
res.status(500).json({ error: 'Failed to reject event' });
res.status(500).json({ error: "Failed to reject event" });
}
}
async getConversations(req: AuthenticatedRequest, res: Response): Promise<void> {
throw new Error('Not implemented');
async getConversations(
req: AuthenticatedRequest,
res: Response,
): Promise<void> {
throw new Error("Not implemented");
}
async getConversation(req: AuthenticatedRequest, res: Response): Promise<void> {
throw new Error('Not implemented');
async getConversation(
req: AuthenticatedRequest,
res: Response,
): Promise<void> {
throw new Error("Not implemented");
}
}

View File

@@ -1,31 +1,34 @@
import { Response } from 'express';
import { EventService } from '../services';
import { AuthenticatedRequest } from '../middleware';
import { Response } from "express";
import { EventService } from "../services";
import { AuthenticatedRequest } from "../middleware";
export class EventController {
constructor(private eventService: EventService) {}
async create(req: AuthenticatedRequest, res: Response): Promise<void> {
throw new Error('Not implemented');
throw new Error("Not implemented");
}
async getById(req: AuthenticatedRequest, res: Response): Promise<void> {
throw new Error('Not implemented');
throw new Error("Not implemented");
}
async getAll(req: AuthenticatedRequest, res: Response): Promise<void> {
throw new Error('Not implemented');
throw new Error("Not implemented");
}
async getByDateRange(req: AuthenticatedRequest, res: Response): Promise<void> {
throw new Error('Not implemented');
async getByDateRange(
req: AuthenticatedRequest,
res: Response,
): Promise<void> {
throw new Error("Not implemented");
}
async update(req: AuthenticatedRequest, res: Response): Promise<void> {
throw new Error('Not implemented');
throw new Error("Not implemented");
}
async delete(req: AuthenticatedRequest, res: Response): Promise<void> {
throw new Error('Not implemented');
throw new Error("Not implemented");
}
}

View File

@@ -1,3 +1,3 @@
export * from './AuthController';
export * from './ChatController';
export * from './EventController';
export * from "./AuthController";
export * from "./ChatController";
export * from "./EventController";

View File

@@ -1,16 +1,20 @@
import { Request, Response, NextFunction } from 'express';
import { verifyToken, TokenPayload } from '../utils/jwt';
import { Request, Response, NextFunction } from "express";
import { verifyToken, TokenPayload } from "../utils/jwt";
export interface AuthenticatedRequest extends Request {
user?: TokenPayload;
}
export function authenticate(req: AuthenticatedRequest, res: Response, next: NextFunction): void {
export function authenticate(
req: AuthenticatedRequest,
res: Response,
next: NextFunction,
): void {
// TODO: Implement real JWT verification
// Fake user for testing purposes
req.user = {
userId: 'fake-user-id',
email: 'test@example.com',
userId: "fake-user-id",
email: "test@example.com",
};
next();
}

View File

@@ -1 +1 @@
export * from './AuthMiddleware';
export * from "./AuthMiddleware";

View File

@@ -1 +1 @@
export * from './mongo';
export * from "./mongo";

View File

@@ -1,23 +1,34 @@
import { ChatMessage, Conversation, CreateMessageDTO, GetMessagesOptions } from '@caldav/shared';
import { ChatRepository } from '../../services/interfaces';
import { ChatMessageModel, ConversationModel } from './models';
import {
ChatMessage,
Conversation,
CreateMessageDTO,
GetMessagesOptions,
} from "@caldav/shared";
import { ChatRepository } from "../../services/interfaces";
import { ChatMessageModel, ConversationModel } from "./models";
export class MongoChatRepository implements ChatRepository {
// Conversations
async getConversationsByUser(userId: string): Promise<Conversation[]> {
throw new Error('Not implemented');
throw new Error("Not implemented");
}
async createConversation(userId: string): Promise<Conversation> {
throw new Error('Not implemented');
throw new Error("Not implemented");
}
// Messages (cursor-based pagination)
async getMessages(conversationId: string, options?: GetMessagesOptions): Promise<ChatMessage[]> {
throw new Error('Not implemented');
async getMessages(
conversationId: string,
options?: GetMessagesOptions,
): Promise<ChatMessage[]> {
throw new Error("Not implemented");
}
async createMessage(conversationId: string, message: CreateMessageDTO): Promise<ChatMessage> {
throw new Error('Not implemented');
async createMessage(
conversationId: string,
message: CreateMessageDTO,
): Promise<ChatMessage> {
throw new Error("Not implemented");
}
}

View File

@@ -1,6 +1,6 @@
import { CalendarEvent, CreateEventDTO, UpdateEventDTO } from '@caldav/shared';
import { EventRepository } from '../../services/interfaces';
import { EventModel } from './models';
import { CalendarEvent, CreateEventDTO, UpdateEventDTO } from "@caldav/shared";
import { EventRepository } from "../../services/interfaces";
import { EventModel } from "./models";
export class MongoEventRepository implements EventRepository {
async findById(id: string): Promise<CalendarEvent | null> {
@@ -11,15 +11,19 @@ export class MongoEventRepository implements EventRepository {
async findByUserId(userId: string): Promise<CalendarEvent[]> {
const events = await EventModel.find({ userId }).sort({ startTime: 1 });
return events.map(e => e.toJSON() as unknown as CalendarEvent);
return events.map((e) => e.toJSON() as unknown as CalendarEvent);
}
async findByDateRange(userId: string, startDate: Date, endDate: Date): Promise<CalendarEvent[]> {
async findByDateRange(
userId: string,
startDate: Date,
endDate: Date,
): Promise<CalendarEvent[]> {
const events = await EventModel.find({
userId,
startTime: { $gte: startDate, $lte: endDate }
startTime: { $gte: startDate, $lte: endDate },
}).sort({ startTime: 1 });
return events.map(e => e.toJSON() as unknown as CalendarEvent);
return events.map((e) => e.toJSON() as unknown as CalendarEvent);
}
async create(userId: string, data: CreateEventDTO): Promise<CalendarEvent> {
@@ -28,7 +32,10 @@ export class MongoEventRepository implements EventRepository {
return event.toJSON() as unknown as CalendarEvent;
}
async update(id: string, data: UpdateEventDTO): Promise<CalendarEvent | null> {
async update(
id: string,
data: UpdateEventDTO,
): Promise<CalendarEvent | null> {
const event = await EventModel.findByIdAndUpdate(id, data, { new: true });
if (!event) return null;
return event.toJSON() as unknown as CalendarEvent;

View File

@@ -1,10 +1,10 @@
import { User } from '@caldav/shared';
import { UserRepository, CreateUserData } from '../../services/interfaces';
import { UserModel } from './models';
import { User } from "@caldav/shared";
import { UserRepository, CreateUserData } from "../../services/interfaces";
import { UserModel } from "./models";
export class MongoUserRepository implements UserRepository {
async findById(id: string): Promise<User | null> {
throw new Error('Not implemented');
throw new Error("Not implemented");
}
async findByEmail(email: string): Promise<User | null> {

View File

@@ -1,3 +1,3 @@
export * from './MongoUserRepository';
export * from './MongoEventRepository';
export * from './MongoChatRepository';
export * from "./MongoUserRepository";
export * from "./MongoEventRepository";
export * from "./MongoChatRepository";

View File

@@ -1,11 +1,18 @@
import mongoose, { Schema, Document, Model } from 'mongoose';
import { ChatMessage, Conversation, CreateEventDTO, UpdateEventDTO, ProposedEventChange } from '@caldav/shared';
import { IdVirtual } from './types';
import mongoose, { Schema, Document, Model } from "mongoose";
import {
ChatMessage,
Conversation,
CreateEventDTO,
UpdateEventDTO,
ProposedEventChange,
} from "@caldav/shared";
import { IdVirtual } from "./types";
export interface ChatMessageDocument extends Omit<ChatMessage, 'id'>, Document {
export interface ChatMessageDocument extends Omit<ChatMessage, "id">, Document {
toJSON(): ChatMessage;
}
export interface ConversationDocument extends Omit<Conversation, 'id'>, Document {
export interface ConversationDocument
extends Omit<Conversation, "id">, Document {
toJSON(): Conversation;
}
@@ -19,7 +26,7 @@ const EventSchema = new Schema<CreateEventDTO>(
isRecurring: { type: Boolean },
recurrenceRule: { type: String },
},
{ _id: false }
{ _id: false },
);
const UpdatesSchema = new Schema<UpdateEventDTO>(
@@ -32,20 +39,30 @@ const UpdatesSchema = new Schema<UpdateEventDTO>(
isRecurring: { type: Boolean },
recurrenceRule: { type: String },
},
{ _id: false }
{ _id: false },
);
const ProposedChangeSchema = new Schema<ProposedEventChange>(
{
action: { type: String, enum: ['create', 'update', 'delete'], required: true },
action: {
type: String,
enum: ["create", "update", "delete"],
required: true,
},
eventId: { type: String },
event: { type: EventSchema },
updates: { type: UpdatesSchema },
},
{ _id: false }
{ _id: false },
);
const ChatMessageSchema = new Schema<ChatMessageDocument, Model<ChatMessageDocument, {}, {}, IdVirtual>, {}, {}, IdVirtual>(
const ChatMessageSchema = new Schema<
ChatMessageDocument,
Model<ChatMessageDocument, {}, {}, IdVirtual>,
{},
{},
IdVirtual
>(
{
conversationId: {
type: String,
@@ -53,7 +70,7 @@ const ChatMessageSchema = new Schema<ChatMessageDocument, Model<ChatMessageDocum
},
sender: {
type: String,
enum: ['user', 'assistant'],
enum: ["user", "assistant"],
required: true,
},
content: {
@@ -81,10 +98,16 @@ const ChatMessageSchema = new Schema<ChatMessageDocument, Model<ChatMessageDocum
return ret;
},
},
}
},
);
const ConversationSchema = new Schema<ConversationDocument, Model<ConversationDocument, {}, {}, IdVirtual>, {}, {}, IdVirtual>(
const ConversationSchema = new Schema<
ConversationDocument,
Model<ConversationDocument, {}, {}, IdVirtual>,
{},
{},
IdVirtual
>(
{
userId: {
type: String,
@@ -109,8 +132,14 @@ const ConversationSchema = new Schema<ConversationDocument, Model<ConversationDo
return ret;
},
},
}
},
);
export const ChatMessageModel = mongoose.model<ChatMessageDocument>('ChatMessage', ChatMessageSchema);
export const ConversationModel = mongoose.model<ConversationDocument>('Conversation', ConversationSchema);
export const ChatMessageModel = mongoose.model<ChatMessageDocument>(
"ChatMessage",
ChatMessageSchema,
);
export const ConversationModel = mongoose.model<ConversationDocument>(
"Conversation",
ConversationSchema,
);

View File

@@ -1,12 +1,18 @@
import mongoose, { Schema, Document, Model } from 'mongoose';
import { CalendarEvent } from '@caldav/shared';
import { IdVirtual } from './types';
import mongoose, { Schema, Document, Model } from "mongoose";
import { CalendarEvent } from "@caldav/shared";
import { IdVirtual } from "./types";
export interface EventDocument extends Omit<CalendarEvent, 'id'>, Document {
export interface EventDocument extends Omit<CalendarEvent, "id">, Document {
toJSON(): CalendarEvent;
}
const EventSchema = new Schema<EventDocument, Model<EventDocument, {}, {}, IdVirtual>, {}, {}, IdVirtual>(
const EventSchema = new Schema<
EventDocument,
Model<EventDocument, {}, {}, IdVirtual>,
{},
{},
IdVirtual
>(
{
userId: {
type: String,
@@ -58,9 +64,9 @@ const EventSchema = new Schema<EventDocument, Model<EventDocument, {}, {}, IdVir
return ret;
},
},
}
},
);
EventSchema.index({ userId: 1, startTime: 1, endTime: 1 });
export const EventModel = mongoose.model<EventDocument>('Event', EventSchema);
export const EventModel = mongoose.model<EventDocument>("Event", EventSchema);

View File

@@ -1,12 +1,18 @@
import mongoose, { Schema, Document, Model } from 'mongoose';
import { User } from '@caldav/shared';
import { IdVirtual } from './types';
import mongoose, { Schema, Document, Model } from "mongoose";
import { User } from "@caldav/shared";
import { IdVirtual } from "./types";
export interface UserDocument extends Omit<User, 'id'>, Document {
export interface UserDocument extends Omit<User, "id">, Document {
toJSON(): User;
}
const UserSchema = new Schema<UserDocument, Model<UserDocument, {}, {}, IdVirtual>, {}, {}, IdVirtual>(
const UserSchema = new Schema<
UserDocument,
Model<UserDocument, {}, {}, IdVirtual>,
{},
{},
IdVirtual
>(
{
email: {
type: String,
@@ -43,7 +49,7 @@ const UserSchema = new Schema<UserDocument, Model<UserDocument, {}, {}, IdVirtua
return ret;
},
},
}
},
);
export const UserModel = mongoose.model<UserDocument>('User', UserSchema);
export const UserModel = mongoose.model<UserDocument>("User", UserSchema);

View File

@@ -1,3 +1,3 @@
export * from './UserModel';
export * from './EventModel';
export * from './ChatModel';
export * from "./UserModel";
export * from "./EventModel";
export * from "./ChatModel";

View File

@@ -1,13 +1,13 @@
import { Router } from 'express';
import { AuthController } from '../controllers';
import { Router } from "express";
import { AuthController } from "../controllers";
export function createAuthRoutes(authController: AuthController): Router {
const router = Router();
router.post('/login', (req, res) => authController.login(req, res));
router.post('/register', (req, res) => authController.register(req, res));
router.post('/refresh', (req, res) => authController.refresh(req, res));
router.post('/logout', (req, res) => authController.logout(req, res));
router.post("/login", (req, res) => authController.login(req, res));
router.post("/register", (req, res) => authController.register(req, res));
router.post("/refresh", (req, res) => authController.refresh(req, res));
router.post("/logout", (req, res) => authController.logout(req, res));
return router;
}

View File

@@ -1,17 +1,25 @@
import { Router } from 'express';
import { ChatController } from '../controllers';
import { authenticate } from '../middleware';
import { Router } from "express";
import { ChatController } from "../controllers";
import { authenticate } from "../middleware";
export function createChatRoutes(chatController: ChatController): Router {
const router = Router();
router.use(authenticate);
router.post('/message', (req, res) => chatController.sendMessage(req, res));
router.post('/confirm/:conversationId/:messageId', (req, res) => chatController.confirmEvent(req, res));
router.post('/reject/:conversationId/:messageId', (req, res) => chatController.rejectEvent(req, res));
router.get('/conversations', (req, res) => chatController.getConversations(req, res));
router.get('/conversations/:id', (req, res) => chatController.getConversation(req, res));
router.post("/message", (req, res) => chatController.sendMessage(req, res));
router.post("/confirm/:conversationId/:messageId", (req, res) =>
chatController.confirmEvent(req, res),
);
router.post("/reject/:conversationId/:messageId", (req, res) =>
chatController.rejectEvent(req, res),
);
router.get("/conversations", (req, res) =>
chatController.getConversations(req, res),
);
router.get("/conversations/:id", (req, res) =>
chatController.getConversation(req, res),
);
return router;
}

View File

@@ -1,18 +1,18 @@
import { Router } from 'express';
import { EventController } from '../controllers';
import { authenticate } from '../middleware';
import { Router } from "express";
import { EventController } from "../controllers";
import { authenticate } from "../middleware";
export function createEventRoutes(eventController: EventController): Router {
const router = Router();
router.use(authenticate);
router.post('/', (req, res) => eventController.create(req, res));
router.get('/', (req, res) => eventController.getAll(req, res));
router.get('/range', (req, res) => eventController.getByDateRange(req, res));
router.get('/:id', (req, res) => eventController.getById(req, res));
router.put('/:id', (req, res) => eventController.update(req, res));
router.delete('/:id', (req, res) => eventController.delete(req, res));
router.post("/", (req, res) => eventController.create(req, res));
router.get("/", (req, res) => eventController.getAll(req, res));
router.get("/range", (req, res) => eventController.getByDateRange(req, res));
router.get("/:id", (req, res) => eventController.getById(req, res));
router.put("/:id", (req, res) => eventController.update(req, res));
router.delete("/:id", (req, res) => eventController.delete(req, res));
return router;
}

View File

@@ -1,8 +1,12 @@
import { Router } from 'express';
import { createAuthRoutes } from './auth.routes';
import { createChatRoutes } from './chat.routes';
import { createEventRoutes } from './event.routes';
import { AuthController, ChatController, EventController } from '../controllers';
import { Router } from "express";
import { createAuthRoutes } from "./auth.routes";
import { createChatRoutes } from "./chat.routes";
import { createEventRoutes } from "./event.routes";
import {
AuthController,
ChatController,
EventController,
} from "../controllers";
export interface Controllers {
authController: AuthController;
@@ -13,13 +17,13 @@ export interface Controllers {
export function createRoutes(controllers: Controllers): Router {
const router = Router();
router.use('/auth', createAuthRoutes(controllers.authController));
router.use('/chat', createChatRoutes(controllers.chatController));
router.use('/events', createEventRoutes(controllers.eventController));
router.use("/auth", createAuthRoutes(controllers.authController));
router.use("/chat", createChatRoutes(controllers.chatController));
router.use("/events", createEventRoutes(controllers.eventController));
return router;
}
export * from './auth.routes';
export * from './chat.routes';
export * from './event.routes';
export * from "./auth.routes";
export * from "./chat.routes";
export * from "./event.routes";

View File

@@ -1,7 +1,7 @@
import { User, CreateUserDTO, LoginDTO, AuthResponse } from '@caldav/shared';
import { UserRepository } from './interfaces';
import * as jwt from '../utils/jwt';
import * as password from '../utils/password';
import { User, CreateUserDTO, LoginDTO, AuthResponse } from "@caldav/shared";
import { UserRepository } from "./interfaces";
import * as jwt from "../utils/jwt";
import * as password from "../utils/password";
export class AuthService {
constructor(private userRepo: UserRepository) {}
@@ -9,21 +9,21 @@ export class AuthService {
async login(data: LoginDTO): Promise<AuthResponse> {
const user = await this.userRepo.findByEmail(data.email);
if (!user || !user.passwordHash) {
throw new Error('Invalid credentials');
throw new Error("Invalid credentials");
}
const isValid = await password.compare(data.password, user.passwordHash);
if (!isValid) {
throw new Error('Invalid credentials');
throw new Error("Invalid credentials");
}
return { user, accessToken: '' };
return { user, accessToken: "" };
}
async register(data: CreateUserDTO): Promise<AuthResponse> {
const existingUser = await this.userRepo.findByEmail(data.email);
if (existingUser) {
throw new Error('Email already exists');
throw new Error("Email already exists");
}
const passwordHash = await password.hash(data.password);
@@ -33,14 +33,14 @@ export class AuthService {
passwordHash,
});
return { user, accessToken: '' };
return { user, accessToken: "" };
}
async refreshToken(refreshToken: string): Promise<AuthResponse> {
throw new Error('Not implemented');
throw new Error("Not implemented");
}
async logout(userId: string): Promise<void> {
throw new Error('Not implemented');
throw new Error("Not implemented");
}
}

View File

@@ -1,6 +1,17 @@
import { ChatMessage, ChatResponse, SendMessageDTO, ConversationSummary, GetMessagesOptions, ProposedEventChange, getDay, CreateEventDTO, UpdateEventDTO, EventAction } from '@caldav/shared';
import { ChatRepository, EventRepository, AIProvider } from './interfaces';
import { getWeeksOverview, getMonthOverview } from '../utils/eventFormatters';
import {
ChatMessage,
ChatResponse,
SendMessageDTO,
ConversationSummary,
GetMessagesOptions,
ProposedEventChange,
getDay,
CreateEventDTO,
UpdateEventDTO,
EventAction,
} from "@caldav/shared";
import { ChatRepository, EventRepository, AIProvider } from "./interfaces";
import { getWeeksOverview, getMonthOverview } from "../utils/eventFormatters";
type TestResponse = { content: string; proposedChange?: ProposedEventChange };
@@ -9,139 +20,147 @@ let responseIndex = 8;
// Static test responses (event proposals)
const staticResponses: TestResponse[] = [
// {{{
// Response 0: Meeting mit Jens - next Friday 14:00
{
content: "Alles klar! Ich erstelle dir einen Termin für das Meeting mit Jens am nächsten Freitag um 14:00 Uhr:",
proposedChange: {
action: 'create',
event: {
title: "Meeting mit Jens",
startTime: getDay('Friday', 1, 14, 0),
endTime: getDay('Friday', 1, 15, 0),
description: "Arbeitstreffen",
}
}
// {{{
// Response 0: Meeting mit Jens - next Friday 14:00
{
content:
"Alles klar! Ich erstelle dir einen Termin für das Meeting mit Jens am nächsten Freitag um 14:00 Uhr:",
proposedChange: {
action: "create",
event: {
title: "Meeting mit Jens",
startTime: getDay("Friday", 1, 14, 0),
endTime: getDay("Friday", 1, 15, 0),
description: "Arbeitstreffen",
},
},
// Response 1: Recurring event - every Saturday 10:00
{
content: "Verstanden! Ich erstelle einen wiederkehrenden Termin: Jeden Samstag um 10:00 Uhr Badezimmer putzen:",
proposedChange: {
action: 'create',
event: {
title: "Badezimmer putzen",
startTime: getDay('Saturday', 1, 10, 0),
endTime: getDay('Saturday', 1, 11, 0),
isRecurring: true,
recurrenceRule: "FREQ=WEEKLY;BYDAY=SA",
}
}
},
// Response 1: Recurring event - every Saturday 10:00
{
content:
"Verstanden! Ich erstelle einen wiederkehrenden Termin: Jeden Samstag um 10:00 Uhr Badezimmer putzen:",
proposedChange: {
action: "create",
event: {
title: "Badezimmer putzen",
startTime: getDay("Saturday", 1, 10, 0),
endTime: getDay("Saturday", 1, 11, 0),
isRecurring: true,
recurrenceRule: "FREQ=WEEKLY;BYDAY=SA",
},
},
// Response 2: 2-week overview (DYNAMIC - placeholder)
{ content: '' },
// Response 3: Delete "Meeting mit Jens" (DYNAMIC - placeholder)
{ content: '' },
// Response 4: Doctor appointment with description
{
content: "Ich habe dir einen Arzttermin eingetragen. Denk daran, deine Versichertenkarte mitzunehmen!",
proposedChange: {
action: 'create',
event: {
title: "Arzttermin Dr. Müller",
startTime: getDay('Wednesday', 1, 9, 30),
endTime: getDay('Wednesday', 1, 10, 30),
description: "Routineuntersuchung - Versichertenkarte nicht vergessen",
}
}
},
// Response 2: 2-week overview (DYNAMIC - placeholder)
{ content: "" },
// Response 3: Delete "Meeting mit Jens" (DYNAMIC - placeholder)
{ content: "" },
// Response 4: Doctor appointment with description
{
content:
"Ich habe dir einen Arzttermin eingetragen. Denk daran, deine Versichertenkarte mitzunehmen!",
proposedChange: {
action: "create",
event: {
title: "Arzttermin Dr. Müller",
startTime: getDay("Wednesday", 1, 9, 30),
endTime: getDay("Wednesday", 1, 10, 30),
description: "Routineuntersuchung - Versichertenkarte nicht vergessen",
},
},
// Response 5: Birthday - yearly recurring
{
content: "Geburtstage vergisst man leicht - aber nicht mit mir! Ich habe Mamas Geburtstag eingetragen:",
proposedChange: {
action: 'create',
event: {
title: "Mamas Geburtstag",
startTime: getDay('Thursday', 2, 0, 0),
endTime: getDay('Thursday', 2, 23, 59),
isRecurring: true,
recurrenceRule: "FREQ=YEARLY",
}
}
},
// Response 5: Birthday - yearly recurring
{
content:
"Geburtstage vergisst man leicht - aber nicht mit mir! Ich habe Mamas Geburtstag eingetragen:",
proposedChange: {
action: "create",
event: {
title: "Mamas Geburtstag",
startTime: getDay("Thursday", 2, 0, 0),
endTime: getDay("Thursday", 2, 23, 59),
isRecurring: true,
recurrenceRule: "FREQ=YEARLY",
},
},
// Response 6: Gym - recurring for 2 months (8 weeks)
{
content: "Perfekt! Ich habe dein Probetraining eingetragen - jeden Dienstag für die nächsten 2 Monate:",
proposedChange: {
action: 'create',
event: {
title: "Fitnessstudio Probetraining",
startTime: getDay('Tuesday', 1, 18, 0),
endTime: getDay('Tuesday', 1, 19, 30),
isRecurring: true,
recurrenceRule: "FREQ=WEEKLY;BYDAY=TU;COUNT=8",
}
}
},
// Response 6: Gym - recurring for 2 months (8 weeks)
{
content:
"Perfekt! Ich habe dein Probetraining eingetragen - jeden Dienstag für die nächsten 2 Monate:",
proposedChange: {
action: "create",
event: {
title: "Fitnessstudio Probetraining",
startTime: getDay("Tuesday", 1, 18, 0),
endTime: getDay("Tuesday", 1, 19, 30),
isRecurring: true,
recurrenceRule: "FREQ=WEEKLY;BYDAY=TU;COUNT=8",
},
},
// Response 7: 1-week overview (DYNAMIC - placeholder)
{ content: '' },
// Response 8: Help response (text only)
{
content: "Ich bin dein Kalender-Assistent! Du kannst mir einfach sagen, welche Termine du erstellen, ändern oder löschen möchtest. Zum Beispiel:\n\n" +
"• \"Erstelle einen Termin für morgen um 15 Uhr\"\n" +
"• \"Was habe ich nächste Woche vor?\"\n" +
"• \"Verschiebe das Meeting auf Donnerstag\"\n\n" +
"Wie kann ich dir helfen?",
},
// Response 7: 1-week overview (DYNAMIC - placeholder)
{ content: "" },
// Response 8: Help response (text only)
{
content:
"Ich bin dein Kalender-Assistent! Du kannst mir einfach sagen, welche Termine du erstellen, ändern oder löschen möchtest. Zum Beispiel:\n\n" +
'• "Erstelle einen Termin für morgen um 15 Uhr"\n' +
'• "Was habe ich nächste Woche vor?"\n' +
'• "Verschiebe das Meeting auf Donnerstag"\n\n' +
"Wie kann ich dir helfen?",
},
// Response 9: Phone call - short appointment
{
content:
"Alles klar! Ich habe das Telefonat mit deiner Mutter eingetragen:",
proposedChange: {
action: "create",
event: {
title: "Telefonat mit Mama",
startTime: getDay("Sunday", 0, 11, 0),
endTime: getDay("Sunday", 0, 11, 30),
},
},
// Response 9: Phone call - short appointment
{
content: "Alles klar! Ich habe das Telefonat mit deiner Mutter eingetragen:",
proposedChange: {
action: 'create',
event: {
title: "Telefonat mit Mama",
startTime: getDay('Sunday', 0, 11, 0),
endTime: getDay('Sunday', 0, 11, 30),
}
}
},
// Response 10: Update "Telefonat mit Mama" +2 days (DYNAMIC - placeholder)
{ content: "" },
// Response 11: Birthday party - evening event
{
content: "Super! Die Geburtstagsfeier ist eingetragen. Viel Spaß!",
proposedChange: {
action: "create",
event: {
title: "Geburtstagsfeier Lisa",
startTime: getDay("Saturday", 2, 19, 0),
endTime: getDay("Saturday", 2, 23, 0),
description: "Geschenk: Buch über Fotografie",
},
},
// Response 10: Update "Telefonat mit Mama" +2 days (DYNAMIC - placeholder)
{ content: '' },
// Response 11: Birthday party - evening event
{
content: "Super! Die Geburtstagsfeier ist eingetragen. Viel Spaß!",
proposedChange: {
action: 'create',
event: {
title: "Geburtstagsfeier Lisa",
startTime: getDay('Saturday', 2, 19, 0),
endTime: getDay('Saturday', 2, 23, 0),
description: "Geschenk: Buch über Fotografie",
}
}
},
// Response 12: Language course - limited to 8 weeks
{
content:
"Dein Spanischkurs ist eingetragen! Er läuft jeden Donnerstag für die nächsten 8 Wochen:",
proposedChange: {
action: "create",
event: {
title: "Spanischkurs VHS",
startTime: getDay("Thursday", 1, 19, 0),
endTime: getDay("Thursday", 1, 20, 30),
isRecurring: true,
recurrenceRule: "FREQ=WEEKLY;BYDAY=TH;COUNT=8",
},
},
// Response 12: Language course - limited to 8 weeks
{
content: "Dein Spanischkurs ist eingetragen! Er läuft jeden Donnerstag für die nächsten 8 Wochen:",
proposedChange: {
action: 'create',
event: {
title: "Spanischkurs VHS",
startTime: getDay('Thursday', 1, 19, 0),
endTime: getDay('Thursday', 1, 20, 30),
isRecurring: true,
recurrenceRule: "FREQ=WEEKLY;BYDAY=TH;COUNT=8",
}
}
},
// Response 13: Monthly overview (DYNAMIC - placeholder)
{ content: '' },
// }}}
},
// Response 13: Monthly overview (DYNAMIC - placeholder)
{ content: "" },
// }}}
];
async function getTestResponse(
index: number,
eventRepo: EventRepository,
userId: string
userId: string,
): Promise<TestResponse> {
const responseIdx = index % staticResponses.length;
@@ -153,14 +172,15 @@ async function getTestResponse(
if (responseIdx === 3) {
// Delete "Meeting mit Jens"
const events = await eventRepo.findByUserId(userId);
const jensEvent = events.find(e => e.title === 'Meeting mit Jens');
const jensEvent = events.find((e) => e.title === "Meeting mit Jens");
if (jensEvent) {
return {
content: "Alles klar, ich lösche den Termin 'Meeting mit Jens' für dich:",
content:
"Alles klar, ich lösche den Termin 'Meeting mit Jens' für dich:",
proposedChange: {
action: 'delete',
action: "delete",
eventId: jensEvent.id,
}
},
};
}
return { content: "Ich konnte keinen Termin 'Meeting mit Jens' finden." };
@@ -173,16 +193,17 @@ async function getTestResponse(
if (responseIdx === 10) {
// Update "Telefonat mit Mama" +2 days
const events = await eventRepo.findByUserId(userId);
const mamaEvent = events.find(e => e.title === 'Telefonat mit Mama');
const mamaEvent = events.find((e) => e.title === "Telefonat mit Mama");
if (mamaEvent) {
const newStart = new Date(mamaEvent.startTime);
newStart.setDate(newStart.getDate() + 2);
const newEnd = new Date(mamaEvent.endTime);
newEnd.setDate(newEnd.getDate() + 2);
return {
content: "Alles klar, ich verschiebe das Telefonat mit Mama um 2 Tage nach hinten:",
content:
"Alles klar, ich verschiebe das Telefonat mit Mama um 2 Tage nach hinten:",
proposedChange: {
action: 'update',
action: "update",
eventId: mamaEvent.id,
updates: { startTime: newStart, endTime: newEnd },
// Include event with new times for display
@@ -191,8 +212,8 @@ async function getTestResponse(
startTime: newStart,
endTime: newEnd,
description: mamaEvent.description,
}
}
},
},
};
}
return { content: "Ich konnte keinen Termin 'Telefonat mit Mama' finden." };
@@ -200,7 +221,14 @@ async function getTestResponse(
if (responseIdx === 13) {
const now = new Date();
return { content: await getMonthOverview(eventRepo, userId, now.getFullYear(), now.getMonth()) };
return {
content: await getMonthOverview(
eventRepo,
userId,
now.getFullYear(),
now.getMonth(),
),
};
}
return staticResponses[responseIdx];
@@ -210,17 +238,24 @@ export class ChatService {
constructor(
private chatRepo: ChatRepository,
private eventRepo: EventRepository,
private aiProvider: AIProvider
private aiProvider: AIProvider,
) {}
async processMessage(userId: string, data: SendMessageDTO): Promise<ChatResponse> {
const response = await getTestResponse(responseIndex, this.eventRepo, userId);
async processMessage(
userId: string,
data: SendMessageDTO,
): Promise<ChatResponse> {
const response = await getTestResponse(
responseIndex,
this.eventRepo,
userId,
);
responseIndex++;
const message: ChatMessage = {
id: Date.now().toString(),
conversationId: data.conversationId || 'temp-conv-id',
sender: 'assistant',
conversationId: data.conversationId || "temp-conv-id",
sender: "assistant",
content: response.content,
proposedChange: response.proposedChange,
};
@@ -235,49 +270,57 @@ export class ChatService {
action: EventAction,
event?: CreateEventDTO,
eventId?: string,
updates?: UpdateEventDTO
updates?: UpdateEventDTO,
): Promise<ChatResponse> {
let content: string;
if (action === 'create' && event) {
if (action === "create" && event) {
const createdEvent = await this.eventRepo.create(userId, event);
content = `Der Termin "${createdEvent.title}" wurde erstellt.`;
} else if (action === 'update' && eventId && updates) {
} else if (action === "update" && eventId && updates) {
const updatedEvent = await this.eventRepo.update(eventId, updates);
content = updatedEvent
? `Der Termin "${updatedEvent.title}" wurde aktualisiert.`
: 'Termin nicht gefunden.';
} else if (action === 'delete' && eventId) {
: "Termin nicht gefunden.";
} else if (action === "delete" && eventId) {
await this.eventRepo.delete(eventId);
content = 'Der Termin wurde gelöscht.';
content = "Der Termin wurde gelöscht.";
} else {
content = 'Ungültige Aktion.';
content = "Ungültige Aktion.";
}
const message: ChatMessage = {
id: Date.now().toString(),
conversationId,
sender: 'assistant',
sender: "assistant",
content,
};
return { message, conversationId };
}
async rejectEvent(userId: string, conversationId: string, messageId: string): Promise<ChatResponse> {
async rejectEvent(
userId: string,
conversationId: string,
messageId: string,
): Promise<ChatResponse> {
const message: ChatMessage = {
id: Date.now().toString(),
conversationId,
sender: 'assistant',
content: 'Der Vorschlag wurde abgelehnt.',
sender: "assistant",
content: "Der Vorschlag wurde abgelehnt.",
};
return { message, conversationId };
}
async getConversations(userId: string): Promise<ConversationSummary[]> {
throw new Error('Not implemented');
throw new Error("Not implemented");
}
async getConversation(userId: string, conversationId: string, options?: GetMessagesOptions): Promise<ChatMessage[]> {
throw new Error('Not implemented');
async getConversation(
userId: string,
conversationId: string,
options?: GetMessagesOptions,
): Promise<ChatMessage[]> {
throw new Error("Not implemented");
}
}

View File

@@ -1,30 +1,38 @@
import { CalendarEvent, CreateEventDTO, UpdateEventDTO } from '@caldav/shared';
import { EventRepository } from './interfaces';
import { CalendarEvent, CreateEventDTO, UpdateEventDTO } from "@caldav/shared";
import { EventRepository } from "./interfaces";
export class EventService {
constructor(private eventRepo: EventRepository) {}
async create(userId: string, data: CreateEventDTO): Promise<CalendarEvent> {
throw new Error('Not implemented');
throw new Error("Not implemented");
}
async getById(id: string, userId: string): Promise<CalendarEvent | null> {
throw new Error('Not implemented');
throw new Error("Not implemented");
}
async getAll(userId: string): Promise<CalendarEvent[]> {
throw new Error('Not implemented');
throw new Error("Not implemented");
}
async getByDateRange(userId: string, startDate: Date, endDate: Date): Promise<CalendarEvent[]> {
throw new Error('Not implemented');
async getByDateRange(
userId: string,
startDate: Date,
endDate: Date,
): Promise<CalendarEvent[]> {
throw new Error("Not implemented");
}
async update(id: string, userId: string, data: UpdateEventDTO): Promise<CalendarEvent | null> {
throw new Error('Not implemented');
async update(
id: string,
userId: string,
data: UpdateEventDTO,
): Promise<CalendarEvent | null> {
throw new Error("Not implemented");
}
async delete(id: string, userId: string): Promise<boolean> {
throw new Error('Not implemented');
throw new Error("Not implemented");
}
}

View File

@@ -1,4 +1,4 @@
export * from './AuthService';
export * from './ChatService';
export * from './EventService';
export * from './interfaces';
export * from "./AuthService";
export * from "./ChatService";
export * from "./EventService";
export * from "./interfaces";

View File

@@ -1,4 +1,8 @@
import { CalendarEvent, ChatMessage, ProposedEventChange } from '@caldav/shared';
import {
CalendarEvent,
ChatMessage,
ProposedEventChange,
} from "@caldav/shared";
export interface AIContext {
userId: string;

View File

@@ -1,4 +1,9 @@
import { ChatMessage, Conversation, CreateMessageDTO, GetMessagesOptions } from '@caldav/shared';
import {
ChatMessage,
Conversation,
CreateMessageDTO,
GetMessagesOptions,
} from "@caldav/shared";
export interface ChatRepository {
// Conversations
@@ -6,6 +11,12 @@ export interface ChatRepository {
createConversation(userId: string): Promise<Conversation>;
// Messages (cursor-based pagination)
getMessages(conversationId: string, options?: GetMessagesOptions): Promise<ChatMessage[]>;
createMessage(conversationId: string, message: CreateMessageDTO): Promise<ChatMessage>;
getMessages(
conversationId: string,
options?: GetMessagesOptions,
): Promise<ChatMessage[]>;
createMessage(
conversationId: string,
message: CreateMessageDTO,
): Promise<ChatMessage>;
}

View File

@@ -1,9 +1,13 @@
import { CalendarEvent, CreateEventDTO, UpdateEventDTO } from '@caldav/shared';
import { CalendarEvent, CreateEventDTO, UpdateEventDTO } from "@caldav/shared";
export interface EventRepository {
findById(id: string): Promise<CalendarEvent | null>;
findByUserId(userId: string): Promise<CalendarEvent[]>;
findByDateRange(userId: string, startDate: Date, endDate: Date): Promise<CalendarEvent[]>;
findByDateRange(
userId: string,
startDate: Date,
endDate: Date,
): Promise<CalendarEvent[]>;
create(userId: string, data: CreateEventDTO): Promise<CalendarEvent>;
update(id: string, data: UpdateEventDTO): Promise<CalendarEvent | null>;
delete(id: string): Promise<boolean>;

View File

@@ -1,4 +1,4 @@
import { User } from '@caldav/shared';
import { User } from "@caldav/shared";
export interface CreateUserData {
email: string;

View File

@@ -1,4 +1,4 @@
export * from './AIProvider';
export * from './UserRepository';
export * from './EventRepository';
export * from './ChatRepository';
export * from "./AIProvider";
export * from "./UserRepository";
export * from "./EventRepository";
export * from "./ChatRepository";

View File

@@ -4,34 +4,37 @@ import {
DAY_TO_GERMAN,
DAY_TO_GERMAN_SHORT,
MONTH_TO_GERMAN,
} from '@caldav/shared';
import { EventRepository } from '../services/interfaces';
import { expandRecurringEvents, ExpandedEvent } from './recurrenceExpander';
} from "@caldav/shared";
import { EventRepository } from "../services/interfaces";
import { expandRecurringEvents, ExpandedEvent } from "./recurrenceExpander";
// Private formatting helpers
function formatTime(date: Date): string {
const hours = date.getHours().toString().padStart(2, '0');
const minutes = date.getMinutes().toString().padStart(2, '0');
const hours = date.getHours().toString().padStart(2, "0");
const minutes = date.getMinutes().toString().padStart(2, "0");
return `${hours}:${minutes}`;
}
function formatDateShort(date: Date): string {
const day = date.getDate().toString().padStart(2, '0');
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const day = date.getDate().toString().padStart(2, "0");
const month = (date.getMonth() + 1).toString().padStart(2, "0");
return `${day}.${month}.`;
}
function getWeekNumber(date: Date): number {
const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
const d = new Date(
Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()),
);
const dayNum = d.getUTCDay() || 7;
d.setUTCDate(d.getUTCDate() + 4 - dayNum);
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
return Math.ceil((((d.getTime() - yearStart.getTime()) / 86400000) + 1) / 7);
return Math.ceil(((d.getTime() - yearStart.getTime()) / 86400000 + 1) / 7);
}
function formatWeeksText(events: ExpandedEvent[], weeks: number): string {
const weeksText = weeks === 1 ? 'die nächste Woche' : `die nächsten ${weeks} Wochen`;
const weeksText =
weeks === 1 ? "die nächste Woche" : `die nächsten ${weeks} Wochen`;
if (events.length === 0) {
return `Du hast für ${weeksText} keine Termine.`;
@@ -47,8 +50,10 @@ function formatWeeksText(events: ExpandedEvent[], weeks: number): string {
lines.push(`${weekday}, ${dateStr} - ${timeStr} Uhr: ${event.title}`);
}
lines.push(`\nInsgesamt ${events.length} Termin${events.length === 1 ? '' : 'e'}.`);
return lines.join('\n');
lines.push(
`\nInsgesamt ${events.length} Termin${events.length === 1 ? "" : "e"}.`,
);
return lines.join("\n");
}
function formatMonthText(events: ExpandedEvent[], monthName: string): string {
@@ -66,13 +71,17 @@ function formatMonthText(events: ExpandedEvent[], monthName: string): string {
weekGroups.get(weekNum)!.push(event);
}
const lines: string[] = [`Hier ist deine Monatsübersicht für ${monthName}:\n`];
const lines: string[] = [
`Hier ist deine Monatsübersicht für ${monthName}:\n`,
];
// Sort weeks and format
const sortedWeeks = Array.from(weekGroups.keys()).sort((a, b) => a - b);
for (const weekNum of sortedWeeks) {
const weekEvents = weekGroups.get(weekNum)!;
lines.push(`KW ${weekNum}: ${weekEvents.length} Termin${weekEvents.length === 1 ? '' : 'e'}`);
lines.push(
`KW ${weekNum}: ${weekEvents.length} Termin${weekEvents.length === 1 ? "" : "e"}`,
);
for (const event of weekEvents) {
const day = DAY_INDEX_TO_DAY[event.occurrenceStart.getDay()];
@@ -81,11 +90,13 @@ function formatMonthText(events: ExpandedEvent[], monthName: string): string {
const timeStr = formatTime(event.occurrenceStart);
lines.push(`${weekdayShort} ${dateStr}, ${timeStr}: ${event.title}`);
}
lines.push('');
lines.push("");
}
lines.push(`Insgesamt ${events.length} Termin${events.length === 1 ? '' : 'e'} im ${monthName}.`);
return lines.join('\n');
lines.push(
`Insgesamt ${events.length} Termin${events.length === 1 ? "" : "e"} im ${monthName}.`,
);
return lines.join("\n");
}
// Public API
@@ -97,7 +108,7 @@ function formatMonthText(events: ExpandedEvent[], monthName: string): string {
export async function getWeeksOverview(
eventRepo: EventRepository,
userId: string,
weeks: number
weeks: number,
): Promise<string> {
const now = new Date();
const endDate = new Date(now.getTime() + weeks * 7 * 24 * 60 * 60 * 1000);
@@ -114,7 +125,7 @@ export async function getMonthOverview(
eventRepo: EventRepository,
userId: string,
year: number,
month: number
month: number,
): Promise<string> {
const startOfMonth = new Date(year, month, 1);
const endOfMonth = new Date(year, month + 1, 0, 23, 59, 59);

View File

@@ -1,2 +1,2 @@
export * from './jwt';
export * from './password';
export * from "./jwt";
export * from "./password";

View File

@@ -1,4 +1,4 @@
import jwt from 'jsonwebtoken';
import jwt from "jsonwebtoken";
export interface TokenPayload {
userId: string;
@@ -10,17 +10,17 @@ export interface JWTConfig {
expiresIn: string;
}
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || '1h';
const JWT_SECRET = process.env.JWT_SECRET || "your-secret-key";
const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || "1h";
export function signToken(payload: TokenPayload): string {
throw new Error('Not implemented');
throw new Error("Not implemented");
}
export function verifyToken(token: string): TokenPayload {
throw new Error('Not implemented');
throw new Error("Not implemented");
}
export function decodeToken(token: string): TokenPayload | null {
throw new Error('Not implemented');
throw new Error("Not implemented");
}

View File

@@ -1,4 +1,4 @@
import bcrypt from 'bcrypt';
import bcrypt from "bcrypt";
const SALT_ROUNDS = 10;
@@ -6,6 +6,9 @@ export async function hash(password: string): Promise<string> {
return bcrypt.hash(password, SALT_ROUNDS);
}
export async function compare(password: string, hash: string): Promise<boolean> {
export async function compare(
password: string,
hash: string,
): Promise<boolean> {
return bcrypt.compare(password, hash);
}

View File

@@ -1,5 +1,5 @@
import { RRule, rrulestr } from 'rrule';
import { CalendarEvent } from '@caldav/shared';
import { RRule, rrulestr } from "rrule";
import { CalendarEvent } from "@caldav/shared";
export interface ExpandedEvent extends CalendarEvent {
occurrenceStart: Date;
@@ -9,14 +9,16 @@ export interface ExpandedEvent extends CalendarEvent {
// Convert local time to "fake UTC" for rrule
// rrule interprets all dates as UTC internally, so we need to trick it
function toRRuleDate(date: Date): Date {
return new Date(Date.UTC(
date.getFullYear(),
date.getMonth(),
date.getDate(),
date.getHours(),
date.getMinutes(),
date.getSeconds()
));
return new Date(
Date.UTC(
date.getFullYear(),
date.getMonth(),
date.getDate(),
date.getHours(),
date.getMinutes(),
date.getSeconds(),
),
);
}
// Convert rrule result back to local time
@@ -27,7 +29,7 @@ function fromRRuleDate(date: Date): Date {
date.getUTCDate(),
date.getUTCHours(),
date.getUTCMinutes(),
date.getUTCSeconds()
date.getUTCSeconds(),
);
}
@@ -38,7 +40,7 @@ function fromRRuleDate(date: Date): Date {
export function expandRecurringEvents(
events: CalendarEvent[],
rangeStart: Date,
rangeEnd: Date
rangeEnd: Date,
): ExpandedEvent[] {
const expanded: ExpandedEvent[] = [];
@@ -61,13 +63,15 @@ export function expandRecurringEvents(
// Recurring event: parse RRULE and expand
try {
const rule = rrulestr(`DTSTART:${formatRRuleDateString(startTime)}\nRRULE:${event.recurrenceRule}`);
const rule = rrulestr(
`DTSTART:${formatRRuleDateString(startTime)}\nRRULE:${event.recurrenceRule}`,
);
// Get occurrences within the range (using fake UTC dates)
const occurrences = rule.between(
toRRuleDate(rangeStart),
toRRuleDate(rangeEnd),
true // inclusive
true, // inclusive
);
for (const occurrence of occurrences) {
@@ -82,7 +86,10 @@ export function expandRecurringEvents(
}
} catch (error) {
// If RRULE parsing fails, include the event as a single occurrence
console.error(`Failed to parse recurrence rule for event ${event.id}:`, error);
console.error(
`Failed to parse recurrence rule for event ${event.id}:`,
error,
);
expanded.push({
...event,
occurrenceStart: startTime,
@@ -92,7 +99,9 @@ export function expandRecurringEvents(
}
// Sort by occurrence start time
expanded.sort((a, b) => a.occurrenceStart.getTime() - b.occurrenceStart.getTime());
expanded.sort(
(a, b) => a.occurrenceStart.getTime() - b.occurrenceStart.getTime(),
);
return expanded;
}
@@ -100,10 +109,10 @@ export function expandRecurringEvents(
// Format date as RRULE DTSTART string (YYYYMMDDTHHMMSS)
function formatRRuleDateString(date: Date): string {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
const hours = String(date.getHours()).padStart(2, "0");
const minutes = String(date.getMinutes()).padStart(2, "0");
const seconds = String(date.getSeconds()).padStart(2, "0");
return `${year}${month}${day}T${hours}${minutes}${seconds}`;
}