Skip to content
Draft
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
38 changes: 38 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Dependencies
node_modules/

# Database
tasks.db
*.db

# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Environment variables
.env
.env.local
.env.*.local

# OS files
.DS_Store
Thumbs.db

# IDE
.vscode/
.idea/
*.swp
*.swo
*~

# Test coverage
coverage/
.nyc_output/

# Build artifacts
dist/
build/
*.tgz
181 changes: 180 additions & 1 deletion database.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,32 @@ const initialize = () => {
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`);

// Create labels table
db.run(`
CREATE TABLE IF NOT EXISTS labels (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
color TEXT DEFAULT '#808080',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`);

// Create task_labels junction table for many-to-many relationship
db.run(`
CREATE TABLE IF NOT EXISTS task_labels (
task_id INTEGER NOT NULL,
label_id INTEGER NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (task_id, label_id),
FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE,
FOREIGN KEY (label_id) REFERENCES labels(id) ON DELETE CASCADE
)
`);

// Create indexes for optimal query performance
db.run(`CREATE INDEX IF NOT EXISTS idx_task_labels_task_id ON task_labels(task_id)`);
db.run(`CREATE INDEX IF NOT EXISTS idx_task_labels_label_id ON task_labels(label_id)`);
});
};

Expand Down Expand Up @@ -54,12 +80,165 @@ const closeDatabase = () => {
db.close();
};

// Label operations
const createLabel = (name, color, callback) => {
const sql = `INSERT INTO labels (name, color) VALUES (?, ?)`;
db.run(sql, [name, color || '#808080'], function(err) {
callback(err, this.lastID);
});
};

const getAllLabels = (callback) => {
const sql = `SELECT * FROM labels ORDER BY name ASC`;
db.all(sql, [], callback);
};

const getLabelById = (id, callback) => {
const sql = `SELECT * FROM labels WHERE id = ?`;
db.get(sql, [id], callback);
};

// Optimized function to get labels for multiple tasks in a single query
// This avoids N+1 query problem
const getTaskLabelsOptimized = (taskIds, callback) => {
if (!taskIds || taskIds.length === 0) {
return callback(null, {});
}

const placeholders = taskIds.map(() => '?').join(',');
const sql = `
SELECT
tl.task_id,
l.id,
l.name,
l.color,
l.created_at
FROM task_labels tl
INNER JOIN labels l ON tl.label_id = l.id
WHERE tl.task_id IN (${placeholders})
ORDER BY l.name ASC
`;

db.all(sql, taskIds, (err, rows) => {
if (err) {
return callback(err);
}

// Group labels by task_id for easy lookup
const labelsByTask = {};
rows.forEach(row => {
if (!labelsByTask[row.task_id]) {
labelsByTask[row.task_id] = [];
}
labelsByTask[row.task_id].push({
id: row.id,
name: row.name,
color: row.color,
created_at: row.created_at
});
});

callback(null, labelsByTask);
});
};

// Get labels for a single task (convenience function)
const getTaskLabels = (taskId, callback) => {
getTaskLabelsOptimized([taskId], (err, labelsByTask) => {
if (err) {
return callback(err);
}
callback(null, labelsByTask[taskId] || []);
});
};

const assignLabelToTask = (taskId, labelId, callback) => {
const sql = `INSERT OR IGNORE INTO task_labels (task_id, label_id) VALUES (?, ?)`;
db.run(sql, [taskId, labelId], callback);
};

const removeLabelFromTask = (taskId, labelId, callback) => {
const sql = `DELETE FROM task_labels WHERE task_id = ? AND label_id = ?`;
db.run(sql, [taskId, labelId], callback);
};

// Bulk operations for better performance
const bulkAssignLabels = (assignments, callback) => {
if (!assignments || assignments.length === 0) {
return callback(null);
}

let completed = 0;
let callbackCalled = false;

assignments.forEach(({ taskId, labelId }) => {
db.run(
`INSERT OR IGNORE INTO task_labels (task_id, label_id) VALUES (?, ?)`,
[taskId, labelId],
(err) => {
if (callbackCalled) return;

if (err) {
callbackCalled = true;
return callback(err);
}

completed++;
if (completed === assignments.length) {
callbackCalled = true;
callback(null);
}
}
);
});
};

const bulkRemoveLabels = (removals, callback) => {
if (!removals || removals.length === 0) {
return callback(null);
}

let completed = 0;
let callbackCalled = false;

removals.forEach(({ taskId, labelId }) => {
db.run(
`DELETE FROM task_labels WHERE task_id = ? AND label_id = ?`,
[taskId, labelId],
(err) => {
if (callbackCalled) return;

if (err) {
callbackCalled = true;
return callback(err);
}

completed++;
if (completed === removals.length) {
callbackCalled = true;
callback(null);
}
}
);
});
};

module.exports = {
initialize,
createTask,
getAllTasks,
getTaskById,
updateTask,
deleteTask,
closeDatabase
closeDatabase,
// Label operations
createLabel,
getAllLabels,
getLabelById,
getTaskLabels,
getTaskLabelsOptimized,
assignLabelToTask,
removeLabelFromTask,
bulkAssignLabels,
bulkRemoveLabels
};
Loading