Skip to main content

Overview

The service layer contains all business logic and database operations for the WhatsApp Assistant Bot. Services are implemented as static classes that provide methods for CRUD operations and domain-specific functionality. All services use Drizzle ORM for database operations and follow consistent patterns for error handling and data validation.

Service Architecture

Design Principles

  • Static Methods: Services use static methods (no instantiation required)
  • Database Abstraction: All database queries go through Drizzle ORM
  • Type Safety: Full TypeScript type inference from database schema
  • Single Responsibility: Each service handles one domain

Database Connection

Services import the database instance from src/db/index.ts:
import { db, todos, Todo } from '../db/index.js';
import { eq, and, desc } from 'drizzle-orm';

TodoService

Manages todo list items with support for completion tracking and bulk operations. Location: src/services/TodoService.ts

Methods

create(userId: string, chatId: string, task: string): Promise<Todo>

Creates a new todo item.
const todo = await TodoService.create(
    '[email protected]',
    '[email protected]',
    'Buy groceries'
);

list(chatId: string, includeCompleted = false): Promise<Todo[]>

Lists todos for a specific chat. By default, only shows incomplete todos.
// Get only incomplete todos
const activeTodos = await TodoService.list('[email protected]');

// Get all todos including completed
const allTodos = await TodoService.list('[email protected]', true);

listAllUserTodos(userId: string, includeCompleted = false): Promise<Todo[]>

Lists all todos for a user across all chats.
const userTodos = await TodoService.listAllUserTodos('[email protected]');

complete(chatId: string, todoId: string): Promise<void>

Marks a todo as complete and sets the completion timestamp.
await TodoService.complete('[email protected]', 'uuid-here');

delete(chatId: string, todoId: string): Promise<void>

Permanently deletes a todo item.
await TodoService.delete('[email protected]', 'uuid-here');

clearCompleted(chatId: string): Promise<number>

Deletes all completed todos in a chat and returns the count.
const deletedCount = await TodoService.clearCompleted('[email protected]');
// Returns: 5 (number of todos deleted)

getById(chatId: string, todoId: string): Promise<Todo | null>

Retrieves a specific todo by ID.
const todo = await TodoService.getById('[email protected]', 'uuid-here');
if (todo) {
    console.log(todo.task);
}

Todo Type

interface Todo {
    id: string;
    userId: string;
    chatId: string;
    task: string;
    completed: boolean;
    completedAt: Date | null;
    createdAt: Date;
    updatedAt: Date;
}

NoteService

Handles note creation, retrieval, and search functionality. Location: src/services/NoteService.ts

Methods

create(userId: string, content: string): Promise<Note>

Creates a new note for a user.
const note = await NoteService.create(
    '[email protected]',
    'Remember to call mom on Sunday'
);

list(userId: string): Promise<Note[]>

Returns all notes for a user, sorted by most recently updated.
const notes = await NoteService.list('[email protected]');

getById(userId: string, noteId: string): Promise<Note | null>

Retrieves a specific note by ID.
const note = await NoteService.getById('[email protected]', 'uuid-here');

update(userId: string, noteId: string, content: string): Promise<Note | null>

Updates the content of an existing note.
const updated = await NoteService.update(
    '[email protected]',
    'uuid-here',
    'Updated note content'
);

delete(userId: string, noteId: string): Promise<boolean>

Deletes a note and returns true if successful.
const deleted = await NoteService.delete('[email protected]', 'uuid-here');

search(userId: string, query: string): Promise<Note[]>

Searches notes using case-insensitive pattern matching (PostgreSQL ILIKE).
const results = await NoteService.search('[email protected]', 'grocery');
// Returns up to 10 matching notes

Note Type

interface Note {
    id: string;
    userId: string;
    content: string;
    tags: string[];
    createdAt: Date;
    updatedAt: Date;
}

ReminderService

Manages scheduled reminders with notification integration. Location: src/services/ReminderService.ts

Methods

create(userId: string, task: string, time: Date, notifyUsers: string[] = []): Promise<Reminder>

Creates a reminder and schedules it for notification.
const reminderTime = new Date(Date.now() + 30 * 60000); // 30 minutes from now

const reminder = await ReminderService.create(
    '[email protected]',
    'Call client about project',
    reminderTime,
    ['[email protected]'] // Optional: additional users to notify
);
Note: This automatically schedules the notification via NotificationService.

list(userId: string, includeCompleted = false): Promise<Reminder[]>

Lists reminders for a user, sorted by time.
const upcomingReminders = await ReminderService.list('[email protected]');

delete(userId: string, reminderId: string): Promise<void>

Deletes a reminder and cancels its scheduled notification.
await ReminderService.delete('[email protected]', 'uuid-here');

clearCompleted(userId: string): Promise<number>

Removes all completed reminders for a user.
const clearedCount = await ReminderService.clearCompleted('[email protected]');

Reminder Type

interface Reminder {
    id: string;
    userId: string;
    task: string;
    time: Date;
    notifyUsers: string[];
    isCompleted: boolean;
    createdAt: Date;
}

TimerService

Manages countdown timers with cron-based scheduling. Location: src/services/TimerService.ts

Lifecycle Methods

initialize(socket: WASocket): void

Initializes the service and loads active timers from the database.
// Called in src/index.ts when connection is established
TimerService.initialize(sock);

cleanup(): void

Stops all active timers and clears resources.
// Called when connection is closed
TimerService.cleanup();

Timer Methods

create(userId: string, duration: number): Promise<Timer>

Creates and schedules a timer (duration in minutes).
const timer = await TimerService.create(
    '[email protected]',
    25 // 25 minutes (Pomodoro timer)
);

list(userId: string): Promise<Timer[]>

Lists all active timers for a user.
const activeTimers = await TimerService.list('[email protected]');

cancel(userId: string, timerId: string): Promise<void>

Cancels a timer and stops its notification.
await TimerService.cancel('[email protected]', 'uuid-here');

Implementation Details

  • Uses node-cron for scheduling timer completions
  • Maintains in-memory map of active cron jobs
  • Automatically loads timers on startup
  • Sends WhatsApp notification when timer completes

Timer Type

interface Timer {
    id: string;
    userId: string;
    duration: number; // in minutes
    endTime: Date;
    isActive: boolean;
    createdAt: Date;
}

NotificationService

Central service for scheduling and sending notifications. Location: src/services/NotificationService.ts

Lifecycle Methods

initialize(socket: WASocket): void

Initializes the service and loads all active notifications.
NotificationService.initialize(sock);

cleanup(): void

Stops all scheduled jobs and clears resources.
NotificationService.cleanup();

Scheduling Methods

scheduleReminder(reminder: Reminder): Promise<void>

Schedules a reminder notification using cron.
await NotificationService.scheduleReminder(reminder);

scheduleTimer(timer: Timer): Promise<void>

Schedules a timer completion notification.
await NotificationService.scheduleTimer(timer);

cancelReminder(reminderId: string): void

Cancels a scheduled reminder notification.
NotificationService.cancelReminder('uuid-here');

cancelTimer(timerId: string): void

Cancels a scheduled timer notification.
NotificationService.cancelTimer('uuid-here');

Implementation Details

  • Uses cron expressions to schedule exact notification times
  • Maintains separate maps for reminder and timer jobs
  • Automatically marks reminders/timers as completed after sending
  • Handles multiple recipients for reminders

UserService

Manages user accounts and settings. Location: src/services/UserService.ts

Methods

getOrCreate(userId: string): Promise<User>

Retrieves a user or creates one if it doesn’t exist.
const user = await UserService.getOrCreate('[email protected]');

updateLastActive(userId: string): Promise<void>

Updates the user’s last active timestamp.
await UserService.updateLastActive('[email protected]');
Note: This is called automatically by the message handler for every command.

updateSettings(userId: string, settings: Partial<UserSettings>): Promise<void>

Updates user settings.
await UserService.updateSettings('[email protected]', {
    notificationsEnabled: false,
    timezone: 'America/New_York'
});

getSettings(userId: string): Promise<UserSettings>

Retrieves user settings (creates user if doesn’t exist).
const settings = await UserService.getSettings('[email protected]');
console.log(settings.timezone); // 'UTC'

exists(userId: string): Promise<boolean>

Checks if a user exists in the database.
const userExists = await UserService.exists('[email protected]');

User Types

interface User {
    id: string;
    userId: string;
    lastActive: Date;
    notificationsEnabled: boolean;
    timezone: string;
    spotifyRefreshToken: string | null;
    createdAt: Date;
    updatedAt: Date;
}

interface UserSettings {
    notificationsEnabled: boolean;
    timezone: string;
}

Creating a New Service

1
Step 1: Define the Database Schema
2
Add your table to src/db/schema.ts:
3
export const myTable = pgTable('my_table', {
    id: uuid('id').defaultRandom().primaryKey(),
    userId: text('user_id').notNull(),
    data: text('data').notNull(),
    createdAt: timestamp('created_at').defaultNow(),
});

export type MyData = typeof myTable.$inferSelect;
export type NewMyData = typeof myTable.$inferInsert;
4
Step 2: Create the Service File
5
Create src/services/MyService.ts:
6
import { db, myTable, MyData } from '../db/index.js';
import { eq } from 'drizzle-orm';

export class MyService {
    static async create(userId: string, data: string): Promise<MyData> {
        const [result] = await db.insert(myTable)
            .values({ userId, data })
            .returning();
        return result;
    }

    static async list(userId: string): Promise<MyData[]> {
        return await db.select()
            .from(myTable)
            .where(eq(myTable.userId, userId));
    }

    // Add more methods as needed
}
7
Step 3: Export from Index
8
Add to src/services/index.ts:
9
export { MyService } from './MyService.js';
10
Step 4: Use in Command Handlers
11
import { MyService } from '../../services/MyService.js';

// In your command handler
const data = await MyService.create(sender, 'some data');

Best Practices

1. Always Use Returning

When inserting or updating, use .returning() to get the result:
const [newItem] = await db.insert(table).values(data).returning();
return newItem;

2. Use Drizzle Query Builders

// Good: Type-safe queries
await db.select()
    .from(todos)
    .where(and(
        eq(todos.userId, userId),
        eq(todos.completed, false)
    ));

// Bad: Raw SQL
await db.execute(sql`SELECT * FROM todos WHERE user_id = ${userId}`);

3. Handle Null Results

static async getById(id: string): Promise<Item | null> {
    const result = await db.select()
        .from(items)
        .where(eq(items.id, id))
        .limit(1);
    return result[0] || null; // Handle empty array
}
await db.transaction(async (tx) => {
    await tx.insert(table1).values(data1);
    await tx.insert(table2).values(data2);
});

Next Steps

Build docs developers (and LLMs) love