Skip to main content

Devlog Feb 15: MoLOS-Tasks Development

· 9 min read
Eduardez
MoLOS Lead Developer

This week we focused heavily on the development of the MoLOS-Tasks module, implementing the core repository layer, API endpoints, and database schema. We also enhanced task management features with additional functionality.

Repository Layer Implementation

The foundation of MoLOS-Tasks is its repository layer, which provides a clean abstraction for database operations. We implemented several repositories with comprehensive CRUD operations:

BaseRepository Pattern

We started by creating a BaseRepository class that provides common functionality for all repositories:

export abstract class BaseRepository {
constructor(protected db: BetterSQLite3Database) {}

protected async findById<T>(
table: SQLiteTable,
id: string
): Promise<T | null> {
const result = await this.db
.select()
.from(table)
.where(eq(table.id, id))
.limit(1);

return result[0] || null;
}

protected async findAll<T>(table: SQLiteTable): Promise<T[]> {
return await this.db.select().from(table);
}

protected async deleteById(table: SQLiteTable, id: string): Promise<void> {
await this.db.delete(table).where(eq(table.id, id));
}
}

AreaRepository

The AreaRepository manages high-level organizational categories:

export class AreaRepository extends BaseRepository {
async create(data: CreateAreaDTO): Promise<Area> {
const [area] = await this.db
.insert(areas)
.values({
id: generateId(),
name: data.name,
color: data.color,
icon: data.icon,
userId: data.userId,
createdAt: Date.now(),
updatedAt: Date.now()
})
.returning();

return area;
}

async findByUser(userId: string): Promise<Area[]> {
return await this.db
.select()
.from(areas)
.where(eq(areas.userId, userId))
.orderBy(desc(areas.createdAt));
}

async update(id: string, data: UpdateAreaDTO): Promise<Area> {
const [area] = await this.db
.update(areas)
.set({
...data,
updatedAt: Date.now()
})
.where(eq(areas.id, id))
.returning();

return area;
}
}

ProjectRepository

The ProjectRepository handles project management with key-based identification:

export class ProjectRepository extends BaseRepository {
async create(data: CreateProjectDTO): Promise<Project> {
const [project] = await this.db
.insert(projects)
.values({
id: generateId(),
key: data.key,
name: data.name,
areaId: data.areaId,
color: data.color,
userId: data.userId,
createdAt: Date.now(),
updatedAt: Date.now()
})
.returning();

return project;
}

async findByKey(key: string): Promise<Project | null> {
const result = await this.db
.select()
.from(projects)
.where(eq(projects.key, key))
.limit(1);

return result[0] || null;
}

async findByArea(areaId: string): Promise<Project[]> {
return await this.db
.select()
.from(projects)
.where(eq(projects.areaId, areaId))
.orderBy(desc(projects.createdAt));
}

async updateKey(id: string, newKey: string): Promise<Project> {
// Check if key already exists
const existing = await this.findByKey(newKey);
if (existing && existing.id !== id) {
throw new Error(`Project key '${newKey}' already exists`);
}

// Update project key
const [project] = await this.db
.update(projects)
.set({
key: newKey,
updatedAt: Date.now()
})
.where(eq(projects.id, id))
.returning();

// Update task IDs to reflect new project key
await this.db
.update(tasks)
.set({ updatedAt: Date.now() })
.where(eq(tasks.projectId, id));

return project;
}
}

TaskRepository

The TaskRepository provides comprehensive task management with filtering capabilities:

export class TaskRepository extends BaseRepository {
async create(data: CreateTaskDTO): Promise<Task> {
const [task] = await this.db
.insert(tasks)
.values({
id: generateId(),
projectId: data.projectId,
title: data.title,
description: data.description,
status: data.status || 'todo',
priority: data.priority || 'medium',
dueDate: data.dueDate,
assigneeId: data.assigneeId,
tags: data.tags || [],
userId: data.userId,
createdAt: Date.now(),
updatedAt: Date.now()
})
.returning();

return task;
}

async findByProject(projectId: string): Promise<Task[]> {
return await this.db
.select()
.from(tasks)
.where(eq(tasks.projectId, projectId))
.orderBy(asc(tasks.status), desc(tasks.priority));
}

async findByUser(userId: string): Promise<Task[]> {
return await this.db
.select()
.from(tasks)
.where(eq(tasks.userId, userId))
.orderBy(desc(tasks.createdAt));
}

async findByStatus(status: TaskStatus): Promise<Task[]> {
return await this.db
.select()
.from(tasks)
.where(eq(tasks.status, status))
.orderBy(desc(tasks.createdAt));
}

async update(id: string, data: UpdateTaskDTO): Promise<Task> {
const [task] = await this.db
.update(tasks)
.set({
...data,
updatedAt: Date.now()
})
.where(eq(tasks.id, id))
.returning();

return task;
}

async delete(id: string): Promise<void> {
await this.db.delete(tasks).where(eq(tasks.id, id));
}
}

DailyLogRepository

The DailyLogRepository manages daily activity tracking:

export class DailyLogRepository extends BaseRepository {
async create(data: CreateDailyLogDTO): Promise<DailyLog> {
const [log] = await this.db
.insert(dailyLogs)
.values({
id: generateId(),
userId: data.userId,
date: data.date,
tasksCompleted: data.tasksCompleted || [],
notes: data.notes,
mood: data.mood,
createdAt: Date.now(),
updatedAt: Date.now()
})
.returning();

return log;
}

async findByDate(userId: string, date: string): Promise<DailyLog | null> {
const result = await this.db
.select()
.from(dailyLogs)
.where(
and(
eq(dailyLogs.userId, userId),
eq(dailyLogs.date, date)
)
)
.limit(1);

return result[0] || null;
}

async findByDateRange(
userId: string,
startDate: string,
endDate: string
): Promise<DailyLog[]> {
return await this.db
.select()
.from(dailyLogs)
.where(
and(
eq(dailyLogs.userId, userId),
gte(dailyLogs.date, startDate),
lte(dailyLogs.date, endDate)
)
)
.orderBy(desc(dailyLogs.date));
}
}

SettingsRepository

The SettingsRepository manages user-specific preferences:

export class SettingsRepository extends BaseRepository {
async get(userId: string): Promise<TaskSettings | null> {
const result = await this.db
.select()
.from(taskSettings)
.where(eq(taskSettings.userId, userId))
.limit(1);

return result[0] || null;
}

async upsert(userId: string, data: Partial<TaskSettings>): Promise<TaskSettings> {
const existing = await this.get(userId);

if (existing) {
const [settings] = await this.db
.update(taskSettings)
.set({
...data,
updatedAt: Date.now()
})
.where(eq(taskSettings.userId, userId))
.returning();

return settings;
} else {
const [settings] = await this.db
.insert(taskSettings)
.values({
userId,
...data,
createdAt: Date.now(),
updatedAt: Date.now()
})
.returning();

return settings;
}
}
}

Database Schema Design

We designed a comprehensive database schema to support all features:

export const areas = sqliteTable('tasks_areas', {
id: text('id').primaryKey(),
name: text('name').notNull(),
color: text('color').notNull().default('#3b82f6'),
icon: text('icon'),
userId: text('user_id').notNull(),
createdAt: integer('created_at').notNull(),
updatedAt: integer('updated_at').notNull()
});

export const projects = sqliteTable('tasks_projects', {
id: text('id').primaryKey(),
key: text('key').notNull().unique(),
name: text('name').notNull(),
areaId: text('area_id').notNull().references(() => areas.id),
color: text('color').notNull().default('#8b5cf6'),
description: text('description'),
status: text('status').notNull().default('active'),
userId: text('user_id').notNull(),
createdAt: integer('created_at').notNull(),
updatedAt: integer('updated_at').notNull()
});

export const tasks = sqliteTable('tasks_tasks', {
id: text('id').primaryKey(),
projectId: text('project_id').notNull().references(() => projects.id),
title: text('title').notNull(),
description: text('description'),
status: text('status').notNull().default('todo'),
priority: text('priority').notNull().default('medium'),
dueDate: integer('due_date'),
assigneeId: text('assignee_id'),
tags: text('tags', { mode: 'json' }).$type<string[]>(),
comments: text('comments', { mode: 'json' }).$type<Comment[]>(),
attachments: text('attachments', { mode: 'json' }).$type<Attachment[]>(),
userId: text('user_id').notNull(),
createdAt: integer('created_at').notNull(),
updatedAt: integer('updated_at').notNull()
});

export const dailyLogs = sqliteTable('tasks_daily_logs', {
id: text('id').primaryKey(),
userId: text('user_id').notNull(),
date: text('date').notNull(),
tasksCompleted: text('tasks_completed', { mode: 'json' }).$type<string[]>(),
notes: text('notes'),
mood: text('mood'),
createdAt: integer('created_at').notNull(),
updatedAt: integer('updated_at').notNull()
});

export const taskSettings = sqliteTable('tasks_settings', {
userId: text('user_id').primaryKey(),
defaultView: text('default_view').notNull().default('kanban'),
autoArchive: integer('auto_archive', { mode: 'boolean' }).notNull().default(false),
notificationEmail: text('notification_email'),
createdAt: integer('created_at').notNull(),
updatedAt: integer('updated_at').notNull()
});

API Endpoints

We implemented RESTful API endpoints for all operations:

Areas API

// GET /api/areas
export async function GET({ locals }) {
const areas = await areaRepository.findByUser(locals.user.id);
return json({ areas });
}

// POST /api/areas
export async function POST({ request, locals }) {
const data = await request.json();
const area = await areaRepository.create({
...data,
userId: locals.user.id
});
return json({ area }, { status: 201 });
}

// PUT /api/areas/[id]
export async function PUT({ params, request }) {
const data = await request.json();
const area = await areaRepository.update(params.id, data);
return json({ area });
}

// DELETE /api/areas/[id]
export async function DELETE({ params }) {
await areaRepository.deleteById(areas, params.id);
return json({ success: true });
}

Projects API

// GET /api/projects
export async function GET({ locals }) {
const projects = await projectRepository.findByUser(locals.user.id);
return json({ projects });
}

// POST /api/projects
export async function POST({ request, locals }) {
const data = await request.json();
const project = await projectRepository.create({
...data,
userId: locals.user.id
});
return json({ project }, { status: 201 });
}

// GET /api/projects/[id]
export async function GET({ params }) {
const project = await projectRepository.findById(projects, params.id);
if (!project) {
throw error(404, 'Project not found');
}
return json({ project });
}

// PUT /api/projects/[id]
export async function PUT({ params, request }) {
const data = await request.json();

if (data.key) {
const project = await projectRepository.updateKey(params.id, data.key);
return json({ project });
}

const project = await projectRepository.update(params.id, data);
return json({ project });
}

Tasks API

// GET /api/tasks
export async function GET({ url, locals }) {
const projectId = url.searchParams.get('projectId');
const status = url.searchParams.get('status') as TaskStatus | null;

let tasks: Task[];

if (projectId) {
tasks = await taskRepository.findByProject(projectId);
} else if (status) {
tasks = await taskRepository.findByStatus(status);
} else {
tasks = await taskRepository.findByUser(locals.user.id);
}

return json({ tasks });
}

// POST /api/tasks
export async function POST({ request, locals }) {
const data = await request.json();
const task = await taskRepository.create({
...data,
userId: locals.user.id
});
return json({ task }, { status: 201 });
}

// GET /api/tasks/[id]
export async function GET({ params }) {
const task = await taskRepository.findById(tasks, params.id);
if (!task) {
throw error(404, 'Task not found');
}
return json({ task });
}

// PUT /api/tasks/[id]
export async function PUT({ params, request }) {
const data = await request.json();
const task = await taskRepository.update(params.id, data);
return json({ task });
}

// DELETE /api/tasks/[id]
export async function DELETE({ params }) {
await taskRepository.delete(params.id);
return json({ success: true });
}

Task Management Enhancements

We enhanced task management with several new features:

Task Detail View

Implemented a comprehensive task detail view with editing capabilities:

// src/routes/ui/tasks/[id]/+page.svelte
<script>
export let data;

const task = data.task;
let title = task.title;
let description = task.description;
let status = task.status;
let priority = task.priority;
let dueDate = task.dueDate;

async function updateTask() {
const response = await fetch(`/api/tasks/${task.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
title,
description,
status,
priority,
dueDate
})
});
}
</script>

Kanban Board

Created a drag-and-drop Kanban board for managing tasks:

// src/routes/ui/kanban/+page.svelte
<script>
let tasks = data.tasks;
const columns = ['todo', 'in-progress', 'done'];

function onDragEnd(event) {
const { draggableId, source, destination } = event;

if (!destination) return;

const task = tasks.find(t => t.id === draggableId);
if (task) {
updateTaskStatus(task.id, destination.droppableId as TaskStatus);
}
}

async function updateTaskStatus(taskId: string, status: TaskStatus) {
await fetch(`/api/tasks/${taskId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status })
});
}
</script>

<KanbanBoard
{tasks}
{columns}
on:dragEnd={onDragEnd}
/>

Project Key Management

Implemented project key change confirmation:

async function handleKeyChange(newKey: string) {
if (newKey === project.key) return;

// Check if key exists
const existing = await fetch(`/api/projects/key/${newKey}`);
if (existing.ok) {
alert('Project key already exists');
return;
}

// Confirm change
if (confirm(`Change project key from "${project.key}" to "${newKey}"?`)) {
await fetch(`/api/projects/${project.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ key: newKey })
});

// Reload tasks to update IDs
window.location.reload();
}
}

Testing

We implemented comprehensive unit tests for all repositories:

describe('AreaRepository', () => {
let repo: AreaRepository;
let db: BetterSQLite3Database;

beforeEach(() => {
db = createTestDatabase();
repo = new AreaRepository(db);
});

it('should create an area', async () => {
const area = await repo.create({
name: 'Work',
color: '#3b82f6',
userId: 'user_123'
});

expect(area.id).toBeDefined();
expect(area.name).toBe('Work');
});

it('should find areas by user', async () => {
await repo.create({
name: 'Work',
color: '#3b82f6',
userId: 'user_123'
});

await repo.create({
name: 'Personal',
color: '#10b981',
userId: 'user_123'
});

const areas = await repo.findByUser('user_123');
expect(areas).toHaveLength(2);
});
});

Key Commits

What's Next

With the core repository layer and API endpoints in place, we're now focusing on:

  • Performance: Optimizing queries for larger datasets
  • AI Integration: Expanding AI-powered task suggestions
  • UI Improvements: Enhancing the user interface based on feedback
  • Calendar Integration: Syncing with external calendars
  • Mobile Optimization: Improving the mobile experience
  • Testing: Expanding test coverage for all components