javascript

exercises

exercises.js⚔
/**
 * 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();
Exercises - JavaScript Tutorial | DeepML