javascript
exercises
exercises.jsā”javascript
/**
* IndexedDB Exercises
*
* Practice client-side database storage with IndexedDB
*/
// =============================================================================
// Exercise 1: Basic Database Setup
// =============================================================================
/**
* Create a database called 'taskManager' with an object store called 'tasks'.
* The tasks store should:
* - Use 'id' as the keyPath with autoIncrement
* - Have indexes for: 'status', 'priority', 'dueDate', 'category'
*
* @returns {Promise<IDBDatabase>} The opened database
*/
function createTaskDatabase() {
return new Promise((resolve, reject) => {
// TODO: Implement database creation
// 1. Open database 'taskManager' version 1
// 2. In onupgradeneeded, create 'tasks' object store
// 3. Create indexes for efficient querying
// 4. Handle success and error events
reject(new Error('Not implemented'));
});
}
// Test
async function testExercise1() {
const db = await createTaskDatabase();
console.assert(
db.name === 'taskManager',
'Database name should be taskManager'
);
console.assert(
db.objectStoreNames.contains('tasks'),
'Should have tasks store'
);
const transaction = db.transaction(['tasks'], 'readonly');
const store = transaction.objectStore('tasks');
console.assert(
store.indexNames.contains('status'),
'Should have status index'
);
console.assert(
store.indexNames.contains('priority'),
'Should have priority index'
);
db.close();
console.log('Exercise 1 passed!');
}
// =============================================================================
// Exercise 2: CRUD Task Manager
// =============================================================================
/**
* Implement a TaskManager class with CRUD operations
*/
class TaskManager {
constructor() {
this.db = null;
this.dbName = 'taskManagerApp';
this.storeName = 'tasks';
}
/**
* Initialize the database
*/
async init() {
// TODO: Open database and create store with indexes
// Store should have: status, priority, dueDate, category indexes
throw new Error('Not implemented');
}
/**
* Add a new task
* @param {Object} task - Task object with title, status, priority, dueDate, category
* @returns {Promise<number>} The new task's id
*/
async addTask(task) {
// TODO: Add task with createdAt timestamp
throw new Error('Not implemented');
}
/**
* Get a task by id
* @param {number} id - Task id
* @returns {Promise<Object>} The task object
*/
async getTask(id) {
// TODO: Get task by primary key
throw new Error('Not implemented');
}
/**
* Update a task
* @param {Object} task - Task object with id
* @returns {Promise<number>} The task id
*/
async updateTask(task) {
// TODO: Update task with updatedAt timestamp
throw new Error('Not implemented');
}
/**
* Delete a task
* @param {number} id - Task id
* @returns {Promise<void>}
*/
async deleteTask(id) {
// TODO: Delete task by id
throw new Error('Not implemented');
}
/**
* Get all tasks
* @returns {Promise<Array>} All tasks
*/
async getAllTasks() {
// TODO: Get all tasks
throw new Error('Not implemented');
}
/**
* Close the database
*/
close() {
// TODO: Close database connection
}
}
// Test
async function testExercise2() {
const manager = new TaskManager();
await manager.init();
// Add task
const id = await manager.addTask({
title: 'Learn IndexedDB',
status: 'pending',
priority: 'high',
category: 'learning',
});
// Get task
const task = await manager.getTask(id);
console.assert(task.title === 'Learn IndexedDB', 'Task title should match');
console.assert(task.createdAt, 'Task should have createdAt');
// Update task
await manager.updateTask({ ...task, status: 'in-progress' });
const updated = await manager.getTask(id);
console.assert(updated.status === 'in-progress', 'Status should be updated');
// Delete task
await manager.deleteTask(id);
const deleted = await manager.getTask(id);
console.assert(deleted === undefined, 'Task should be deleted');
manager.close();
console.log('Exercise 2 passed!');
}
// =============================================================================
// Exercise 3: Query by Index
// =============================================================================
/**
* Extend TaskManager with index-based queries
*/
class TaskQueryManager extends TaskManager {
/**
* Get tasks by status
* @param {string} status - Status to filter by
* @returns {Promise<Array>}
*/
async getTasksByStatus(status) {
// TODO: Query using status index
throw new Error('Not implemented');
}
/**
* Get tasks by priority
* @param {string} priority - Priority to filter by
* @returns {Promise<Array>}
*/
async getTasksByPriority(priority) {
// TODO: Query using priority index
throw new Error('Not implemented');
}
/**
* Get tasks by category
* @param {string} category - Category to filter by
* @returns {Promise<Array>}
*/
async getTasksByCategory(category) {
// TODO: Query using category index
throw new Error('Not implemented');
}
/**
* Get overdue tasks (dueDate before today)
* @returns {Promise<Array>}
*/
async getOverdueTasks() {
// TODO: Query using dueDate index with IDBKeyRange
throw new Error('Not implemented');
}
/**
* Get tasks due within a date range
* @param {Date} startDate
* @param {Date} endDate
* @returns {Promise<Array>}
*/
async getTasksDueInRange(startDate, endDate) {
// TODO: Query with IDBKeyRange.bound()
throw new Error('Not implemented');
}
}
// Test
async function testExercise3() {
const manager = new TaskQueryManager();
await manager.init();
// Add test tasks
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
await manager.addTask({
title: 'Urgent task',
status: 'pending',
priority: 'high',
category: 'work',
dueDate: tomorrow.toISOString(),
});
await manager.addTask({
title: 'Overdue task',
status: 'pending',
priority: 'low',
category: 'personal',
dueDate: yesterday.toISOString(),
});
await manager.addTask({
title: 'Completed task',
status: 'completed',
priority: 'medium',
category: 'work',
dueDate: tomorrow.toISOString(),
});
// Test queries
const pending = await manager.getTasksByStatus('pending');
console.assert(pending.length === 2, 'Should have 2 pending tasks');
const highPriority = await manager.getTasksByPriority('high');
console.assert(highPriority.length === 1, 'Should have 1 high priority task');
const workTasks = await manager.getTasksByCategory('work');
console.assert(workTasks.length === 2, 'Should have 2 work tasks');
const overdue = await manager.getOverdueTasks();
console.assert(overdue.length === 1, 'Should have 1 overdue task');
manager.close();
console.log('Exercise 3 passed!');
}
// =============================================================================
// Exercise 4: Batch Operations
// =============================================================================
/**
* Implement batch operations for efficient bulk updates
*/
class BatchTaskManager extends TaskQueryManager {
/**
* Add multiple tasks in a single transaction
* @param {Array<Object>} tasks - Array of task objects
* @returns {Promise<Array<number>>} Array of new task ids
*/
async addTasks(tasks) {
// TODO: Add all tasks in a single transaction
throw new Error('Not implemented');
}
/**
* Delete multiple tasks in a single transaction
* @param {Array<number>} ids - Array of task ids
* @returns {Promise<void>}
*/
async deleteTasks(ids) {
// TODO: Delete all tasks in a single transaction
throw new Error('Not implemented');
}
/**
* Update status for all tasks matching a filter
* @param {Function} filter - Function to filter tasks
* @param {string} newStatus - New status to set
* @returns {Promise<number>} Number of updated tasks
*/
async bulkUpdateStatus(filter, newStatus) {
// TODO: Use cursor to update matching tasks
throw new Error('Not implemented');
}
/**
* Delete all completed tasks
* @returns {Promise<number>} Number of deleted tasks
*/
async deleteCompletedTasks() {
// TODO: Delete all tasks with status 'completed'
throw new Error('Not implemented');
}
}
// Test
async function testExercise4() {
const manager = new BatchTaskManager();
await manager.init();
// Batch add
const ids = await manager.addTasks([
{ title: 'Task 1', status: 'pending', priority: 'low', category: 'test' },
{
title: 'Task 2',
status: 'pending',
priority: 'medium',
category: 'test',
},
{
title: 'Task 3',
status: 'completed',
priority: 'high',
category: 'test',
},
{ title: 'Task 4', status: 'completed', priority: 'low', category: 'test' },
]);
console.assert(ids.length === 4, 'Should add 4 tasks');
// Bulk update
const updated = await manager.bulkUpdateStatus(
(task) => task.priority === 'low',
'in-progress'
);
console.assert(updated === 2, 'Should update 2 low priority tasks');
// Delete completed
const deleted = await manager.deleteCompletedTasks();
console.assert(deleted === 2, 'Should delete 2 completed tasks');
const remaining = await manager.getAllTasks();
console.assert(remaining.length === 2, 'Should have 2 remaining tasks');
manager.close();
console.log('Exercise 4 passed!');
}
// =============================================================================
// Exercise 5: Search and Filter
// =============================================================================
/**
* Implement advanced search and filtering
*/
class SearchableTaskManager extends BatchTaskManager {
/**
* Search tasks by title (partial match)
* @param {string} searchTerm - Search term
* @returns {Promise<Array>}
*/
async searchByTitle(searchTerm) {
// TODO: Iterate through all tasks and filter by title
// Note: IndexedDB doesn't support native text search
throw new Error('Not implemented');
}
/**
* Filter tasks by multiple criteria
* @param {Object} criteria - Filter criteria
* @returns {Promise<Array>}
*/
async filterTasks(criteria) {
// TODO: Apply multiple filters
// criteria can include: status, priority, category, search
throw new Error('Not implemented');
}
/**
* Get task statistics
* @returns {Promise<Object>} Stats object with counts per status
*/
async getTaskStats() {
// TODO: Count tasks by status
// Return: { total, pending, inProgress, completed }
throw new Error('Not implemented');
}
/**
* Get tasks sorted by a field
* @param {string} field - Field to sort by
* @param {string} direction - 'asc' or 'desc'
* @returns {Promise<Array>}
*/
async getTasksSorted(field, direction = 'asc') {
// TODO: Get all tasks and sort by field
throw new Error('Not implemented');
}
}
// Test
async function testExercise5() {
const manager = new SearchableTaskManager();
await manager.init();
await manager.addTasks([
{
title: 'Learn JavaScript basics',
status: 'completed',
priority: 'high',
category: 'learning',
},
{
title: 'Learn IndexedDB',
status: 'in-progress',
priority: 'high',
category: 'learning',
},
{
title: 'Build todo app',
status: 'pending',
priority: 'medium',
category: 'project',
},
{
title: 'Write documentation',
status: 'pending',
priority: 'low',
category: 'project',
},
]);
// Search
const learnTasks = await manager.searchByTitle('Learn');
console.assert(learnTasks.length === 2, 'Should find 2 tasks with "Learn"');
// Filter
const filtered = await manager.filterTasks({
priority: 'high',
category: 'learning',
});
console.assert(
filtered.length === 2,
'Should find 2 high priority learning tasks'
);
// Stats
const stats = await manager.getTaskStats();
console.assert(stats.total === 4, 'Should have 4 total tasks');
console.assert(stats.pending === 2, 'Should have 2 pending tasks');
// Sorted
const sorted = await manager.getTasksSorted('priority', 'desc');
console.assert(
sorted[0].priority === 'high',
'First task should be high priority'
);
manager.close();
console.log('Exercise 5 passed!');
}
// =============================================================================
// Exercise 6: Offline Sync Queue
// =============================================================================
/**
* Implement an offline-first sync queue pattern
*/
class SyncableTaskManager {
constructor() {
this.db = null;
}
/**
* Initialize with tasks and syncQueue stores
*/
async init() {
// TODO: Create database with:
// - 'tasks' store with localId keyPath
// - 'syncQueue' store for pending sync operations
throw new Error('Not implemented');
}
/**
* Save a task locally and queue for sync
* @param {Object} task - Task object
* @returns {Promise<string>} Local ID
*/
async saveLocal(task) {
// TODO:
// 1. Generate a local UUID
// 2. Save task with syncStatus: 'pending'
// 3. Add to syncQueue
throw new Error('Not implemented');
}
/**
* Get all pending sync items
* @returns {Promise<Array>}
*/
async getPendingSync() {
// TODO: Get all tasks with syncStatus: 'pending'
throw new Error('Not implemented');
}
/**
* Mark a task as synced
* @param {string} localId - Local task ID
* @param {string} serverId - Server-assigned ID
*/
async markSynced(localId, serverId) {
// TODO: Update task with serverId and syncStatus: 'synced'
throw new Error('Not implemented');
}
/**
* Sync pending tasks with a server
* @param {Function} syncFn - Function that syncs a single task and returns serverId
* @returns {Promise<Object>} Sync results
*/
async syncWithServer(syncFn) {
// TODO:
// 1. Get pending tasks
// 2. For each, call syncFn
// 3. Mark as synced on success
// 4. Return { success: number, failed: number }
throw new Error('Not implemented');
}
close() {
if (this.db) this.db.close();
}
}
// Test
async function testExercise6() {
const manager = new SyncableTaskManager();
await manager.init();
// Save locally
const localId1 = await manager.saveLocal({ title: 'Offline task 1' });
const localId2 = await manager.saveLocal({ title: 'Offline task 2' });
// Check pending
const pending = await manager.getPendingSync();
console.assert(pending.length === 2, 'Should have 2 pending tasks');
// Simulate sync
let syncCount = 0;
const results = await manager.syncWithServer(async (task) => {
syncCount++;
return `server-id-${syncCount}`;
});
console.assert(results.success === 2, 'Should sync 2 tasks');
// Check no more pending
const afterSync = await manager.getPendingSync();
console.assert(
afterSync.length === 0,
'Should have no pending tasks after sync'
);
manager.close();
console.log('Exercise 6 passed!');
}
// =============================================================================
// Exercise 7: Database Migration
// =============================================================================
/**
* Implement proper database versioning and migrations
*/
class MigratableDatabase {
constructor(dbName) {
this.dbName = dbName;
this.db = null;
this.migrations = [];
}
/**
* Add a migration
* @param {number} version - Target version
* @param {Function} migrateFn - Migration function (db, transaction) => void
*/
addMigration(version, migrateFn) {
// TODO: Store migration for later execution
throw new Error('Not implemented');
}
/**
* Open database and run necessary migrations
* @returns {Promise<IDBDatabase>}
*/
async open() {
// TODO:
// 1. Determine current version from migrations
// 2. Open database with that version
// 3. In onupgradeneeded, run applicable migrations
throw new Error('Not implemented');
}
/**
* Get current version
*/
get version() {
// TODO: Return highest migration version
throw new Error('Not implemented');
}
close() {
if (this.db) this.db.close();
}
}
// Test
async function testExercise7() {
const migrator = new MigratableDatabase('migrateTest');
// Version 1: Create users store
migrator.addMigration(1, (db) => {
db.createObjectStore('users', { keyPath: 'id', autoIncrement: true });
});
// Version 2: Add email index to users
migrator.addMigration(2, (db, transaction) => {
const store = transaction.objectStore('users');
store.createIndex('email', 'email', { unique: true });
});
// Version 3: Add posts store
migrator.addMigration(3, (db) => {
db.createObjectStore('posts', { keyPath: 'id', autoIncrement: true });
});
const db = await migrator.open();
console.assert(db.version === 3, 'Should be version 3');
console.assert(
db.objectStoreNames.contains('users'),
'Should have users store'
);
console.assert(
db.objectStoreNames.contains('posts'),
'Should have posts store'
);
const transaction = db.transaction(['users'], 'readonly');
const store = transaction.objectStore('users');
console.assert(store.indexNames.contains('email'), 'Should have email index');
migrator.close();
console.log('Exercise 7 passed!');
}
// =============================================================================
// Run All Tests
// =============================================================================
async function runAllTests() {
console.log('Running IndexedDB Exercises...\n');
try {
await testExercise1();
await testExercise2();
await testExercise3();
await testExercise4();
await testExercise5();
await testExercise6();
await testExercise7();
console.log('\nā
All exercises passed!');
} catch (error) {
console.error('\nā Test failed:', error.message);
} finally {
// Cleanup databases
const dbs = ['taskManager', 'taskManagerApp', 'migrateTest'];
for (const name of dbs) {
try {
indexedDB.deleteDatabase(name);
} catch (e) {}
}
}
}
// Export for browser or Node.js
if (typeof module !== 'undefined' && module.exports) {
module.exports = {
TaskManager,
TaskQueryManager,
BatchTaskManager,
SearchableTaskManager,
SyncableTaskManager,
MigratableDatabase,
runAllTests,
};
}
// Uncomment to run tests
// runAllTests();