javascript

examples

examples.js
/**
 * IndexedDB Examples
 *
 * Demonstrates client-side database storage with IndexedDB
 */

// =============================================================================
// 1. Basic Database Operations
// =============================================================================

/**
 * Open or create a database
 */
function openDatabase(name, version = 1) {
  return new Promise((resolve, reject) => {
    const request = indexedDB.open(name, version);

    request.onerror = (event) => {
      console.error('Database error:', event.target.error);
      reject(event.target.error);
    };

    request.onsuccess = (event) => {
      const db = event.target.result;
      console.log(`Database '${name}' opened successfully`);
      resolve(db);
    };

    request.onupgradeneeded = (event) => {
      const db = event.target.result;
      console.log(
        `Upgrading database from version ${event.oldVersion} to ${event.newVersion}`
      );

      // Create object stores
      if (!db.objectStoreNames.contains('users')) {
        const userStore = db.createObjectStore('users', {
          keyPath: 'id',
          autoIncrement: true,
        });

        // Create indexes
        userStore.createIndex('email', 'email', { unique: true });
        userStore.createIndex('name', 'name', { unique: false });
        userStore.createIndex('age', 'age', { unique: false });
        userStore.createIndex('createdAt', 'createdAt', { unique: false });

        console.log('Created users object store with indexes');
      }

      if (!db.objectStoreNames.contains('posts')) {
        const postStore = db.createObjectStore('posts', {
          keyPath: 'id',
          autoIncrement: true,
        });

        postStore.createIndex('authorId', 'authorId');
        postStore.createIndex('category', 'category');
        postStore.createIndex('publishedAt', 'publishedAt');

        console.log('Created posts object store');
      }
    };

    request.onblocked = () => {
      console.warn('Database blocked - close other connections');
    };
  });
}

// =============================================================================
// 2. CRUD Operations
// =============================================================================

/**
 * Add a record to an object store
 */
function addRecord(db, storeName, data) {
  return new Promise((resolve, reject) => {
    const transaction = db.transaction([storeName], 'readwrite');
    const store = transaction.objectStore(storeName);

    // Add timestamp if not present
    if (!data.createdAt) {
      data.createdAt = new Date().toISOString();
    }

    const request = store.add(data);

    request.onsuccess = () => {
      console.log(`Added record with id: ${request.result}`);
      resolve(request.result);
    };

    request.onerror = () => {
      console.error('Add failed:', request.error);
      reject(request.error);
    };
  });
}

/**
 * Get a record by key
 */
function getRecord(db, storeName, key) {
  return new Promise((resolve, reject) => {
    const transaction = db.transaction([storeName], 'readonly');
    const store = transaction.objectStore(storeName);
    const request = store.get(key);

    request.onsuccess = () => {
      resolve(request.result);
    };

    request.onerror = () => {
      reject(request.error);
    };
  });
}

/**
 * Get all records
 */
function getAllRecords(db, storeName) {
  return new Promise((resolve, reject) => {
    const transaction = db.transaction([storeName], 'readonly');
    const store = transaction.objectStore(storeName);
    const request = store.getAll();

    request.onsuccess = () => {
      resolve(request.result);
    };

    request.onerror = () => {
      reject(request.error);
    };
  });
}

/**
 * Update a record (put replaces or adds)
 */
function updateRecord(db, storeName, data) {
  return new Promise((resolve, reject) => {
    const transaction = db.transaction([storeName], 'readwrite');
    const store = transaction.objectStore(storeName);

    data.updatedAt = new Date().toISOString();
    const request = store.put(data);

    request.onsuccess = () => {
      console.log(`Updated record with id: ${request.result}`);
      resolve(request.result);
    };

    request.onerror = () => {
      reject(request.error);
    };
  });
}

/**
 * Delete a record
 */
function deleteRecord(db, storeName, key) {
  return new Promise((resolve, reject) => {
    const transaction = db.transaction([storeName], 'readwrite');
    const store = transaction.objectStore(storeName);
    const request = store.delete(key);

    request.onsuccess = () => {
      console.log(`Deleted record with id: ${key}`);
      resolve();
    };

    request.onerror = () => {
      reject(request.error);
    };
  });
}

// =============================================================================
// 3. Index Queries
// =============================================================================

/**
 * Get record by index
 */
function getByIndex(db, storeName, indexName, value) {
  return new Promise((resolve, reject) => {
    const transaction = db.transaction([storeName], 'readonly');
    const store = transaction.objectStore(storeName);
    const index = store.index(indexName);
    const request = index.get(value);

    request.onsuccess = () => {
      resolve(request.result);
    };

    request.onerror = () => {
      reject(request.error);
    };
  });
}

/**
 * Get all records by index value
 */
function getAllByIndex(db, storeName, indexName, value) {
  return new Promise((resolve, reject) => {
    const transaction = db.transaction([storeName], 'readonly');
    const store = transaction.objectStore(storeName);
    const index = store.index(indexName);
    const request = index.getAll(value);

    request.onsuccess = () => {
      resolve(request.result);
    };

    request.onerror = () => {
      reject(request.error);
    };
  });
}

// =============================================================================
// 4. Range Queries
// =============================================================================

/**
 * Query with key range
 */
function getInRange(db, storeName, indexName, lower, upper, options = {}) {
  return new Promise((resolve, reject) => {
    const {
      lowerOpen = false,
      upperOpen = false,
      direction = 'next', // 'next', 'prev', 'nextunique', 'prevunique'
      limit = null,
    } = options;

    const transaction = db.transaction([storeName], 'readonly');
    const store = transaction.objectStore(storeName);
    const index = indexName ? store.index(indexName) : store;

    let range;
    if (lower !== undefined && upper !== undefined) {
      range = IDBKeyRange.bound(lower, upper, lowerOpen, upperOpen);
    } else if (lower !== undefined) {
      range = IDBKeyRange.lowerBound(lower, lowerOpen);
    } else if (upper !== undefined) {
      range = IDBKeyRange.upperBound(upper, upperOpen);
    }

    const results = [];
    const request = index.openCursor(range, direction);

    request.onsuccess = (event) => {
      const cursor = event.target.result;

      if (cursor && (limit === null || results.length < limit)) {
        results.push(cursor.value);
        cursor.continue();
      } else {
        resolve(results);
      }
    };

    request.onerror = () => {
      reject(request.error);
    };
  });
}

// Example usage with ranges
async function rangeQueryExamples(db) {
  // Get users aged 20-30
  const youngAdults = await getInRange(db, 'users', 'age', 20, 30);
  console.log('Users aged 20-30:', youngAdults);

  // Get users created this week
  const oneWeekAgo = new Date(
    Date.now() - 7 * 24 * 60 * 60 * 1000
  ).toISOString();
  const recentUsers = await getInRange(
    db,
    'users',
    'createdAt',
    oneWeekAgo,
    undefined
  );
  console.log('Recent users:', recentUsers);

  // Get first 5 users (sorted by key, descending)
  const lastFive = await getInRange(db, 'users', null, undefined, undefined, {
    direction: 'prev',
    limit: 5,
  });
  console.log('Last 5 added users:', lastFive);
}

// =============================================================================
// 5. Cursor Operations
// =============================================================================

/**
 * Iterate with cursor for complex operations
 */
function iterateWithCursor(db, storeName, callback, options = {}) {
  return new Promise((resolve, reject) => {
    const { indexName, range, direction = 'next' } = options;

    const transaction = db.transaction([storeName], 'readonly');
    const store = transaction.objectStore(storeName);
    const target = indexName ? store.index(indexName) : store;

    const request = target.openCursor(range, direction);

    request.onsuccess = (event) => {
      const cursor = event.target.result;

      if (cursor) {
        const shouldContinue = callback(cursor.value, cursor.key);

        if (shouldContinue !== false) {
          cursor.continue();
        } else {
          resolve();
        }
      } else {
        resolve(); // No more records
      }
    };

    request.onerror = () => {
      reject(request.error);
    };
  });
}

/**
 * Update records with cursor
 */
function updateWithCursor(db, storeName, filter, updater) {
  return new Promise((resolve, reject) => {
    const transaction = db.transaction([storeName], 'readwrite');
    const store = transaction.objectStore(storeName);
    let updateCount = 0;

    const request = store.openCursor();

    request.onsuccess = (event) => {
      const cursor = event.target.result;

      if (cursor) {
        if (filter(cursor.value)) {
          const updated = updater({ ...cursor.value });
          updated.updatedAt = new Date().toISOString();
          cursor.update(updated);
          updateCount++;
        }
        cursor.continue();
      }
    };

    transaction.oncomplete = () => {
      console.log(`Updated ${updateCount} records`);
      resolve(updateCount);
    };

    transaction.onerror = () => {
      reject(transaction.error);
    };
  });
}

// =============================================================================
// 6. Batch Operations
// =============================================================================

/**
 * Add multiple records in a single transaction
 */
function addBatch(db, storeName, records) {
  return new Promise((resolve, reject) => {
    const transaction = db.transaction([storeName], 'readwrite');
    const store = transaction.objectStore(storeName);
    const ids = [];

    records.forEach((record) => {
      if (!record.createdAt) {
        record.createdAt = new Date().toISOString();
      }
      const request = store.add(record);
      request.onsuccess = () => ids.push(request.result);
    });

    transaction.oncomplete = () => {
      console.log(`Added ${ids.length} records`);
      resolve(ids);
    };

    transaction.onerror = () => {
      reject(transaction.error);
    };
  });
}

/**
 * Delete multiple records
 */
function deleteBatch(db, storeName, keys) {
  return new Promise((resolve, reject) => {
    const transaction = db.transaction([storeName], 'readwrite');
    const store = transaction.objectStore(storeName);

    keys.forEach((key) => store.delete(key));

    transaction.oncomplete = () => {
      console.log(`Deleted ${keys.length} records`);
      resolve();
    };

    transaction.onerror = () => {
      reject(transaction.error);
    };
  });
}

// =============================================================================
// 7. IndexedDB Wrapper Class
// =============================================================================

class IndexedDBWrapper {
  constructor(dbName, version = 1) {
    this.dbName = dbName;
    this.version = version;
    this.db = null;
    this.storeConfigs = new Map();
  }

  /**
   * Configure an object store
   */
  configureStore(name, keyPath = 'id', autoIncrement = true, indexes = []) {
    this.storeConfigs.set(name, { keyPath, autoIncrement, indexes });
    return this;
  }

  /**
   * Open the database
   */
  async open() {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open(this.dbName, this.version);

      request.onerror = () => reject(request.error);
      request.onsuccess = () => {
        this.db = request.result;
        resolve(this);
      };

      request.onupgradeneeded = (event) => {
        const db = event.target.result;

        this.storeConfigs.forEach((config, name) => {
          if (!db.objectStoreNames.contains(name)) {
            const store = db.createObjectStore(name, {
              keyPath: config.keyPath,
              autoIncrement: config.autoIncrement,
            });

            config.indexes.forEach(({ name: indexName, keyPath, options }) => {
              store.createIndex(indexName, keyPath || indexName, options || {});
            });
          }
        });
      };
    });
  }

  /**
   * Get a store accessor
   */
  store(name) {
    return new StoreAccessor(this.db, name);
  }

  /**
   * Close the database
   */
  close() {
    if (this.db) {
      this.db.close();
      this.db = null;
    }
  }

  /**
   * Delete the database
   */
  static async delete(dbName) {
    return new Promise((resolve, reject) => {
      const request = indexedDB.deleteDatabase(dbName);
      request.onsuccess = () => resolve();
      request.onerror = () => reject(request.error);
    });
  }
}

class StoreAccessor {
  constructor(db, storeName) {
    this.db = db;
    this.storeName = storeName;
  }

  _transaction(mode = 'readonly') {
    return this.db
      .transaction([this.storeName], mode)
      .objectStore(this.storeName);
  }

  _promisify(request) {
    return new Promise((resolve, reject) => {
      request.onsuccess = () => resolve(request.result);
      request.onerror = () => reject(request.error);
    });
  }

  async add(data) {
    data.createdAt = data.createdAt || new Date().toISOString();
    return this._promisify(this._transaction('readwrite').add(data));
  }

  async put(data) {
    data.updatedAt = new Date().toISOString();
    return this._promisify(this._transaction('readwrite').put(data));
  }

  async get(key) {
    return this._promisify(this._transaction().get(key));
  }

  async getAll(query, count) {
    return this._promisify(this._transaction().getAll(query, count));
  }

  async delete(key) {
    return this._promisify(this._transaction('readwrite').delete(key));
  }

  async clear() {
    return this._promisify(this._transaction('readwrite').clear());
  }

  async count(query) {
    return this._promisify(this._transaction().count(query));
  }

  index(indexName) {
    return new IndexAccessor(this.db, this.storeName, indexName);
  }
}

class IndexAccessor {
  constructor(db, storeName, indexName) {
    this.db = db;
    this.storeName = storeName;
    this.indexName = indexName;
  }

  _getIndex() {
    return this.db
      .transaction([this.storeName], 'readonly')
      .objectStore(this.storeName)
      .index(this.indexName);
  }

  _promisify(request) {
    return new Promise((resolve, reject) => {
      request.onsuccess = () => resolve(request.result);
      request.onerror = () => reject(request.error);
    });
  }

  async get(key) {
    return this._promisify(this._getIndex().get(key));
  }

  async getAll(query, count) {
    return this._promisify(this._getIndex().getAll(query, count));
  }

  async count(query) {
    return this._promisify(this._getIndex().count(query));
  }
}

// =============================================================================
// 8. Offline Data Sync Example
// =============================================================================

class OfflineStore {
  constructor(dbName) {
    this.dbName = dbName;
    this.db = null;
    this.syncQueue = [];
  }

  async init() {
    const wrapper = new IndexedDBWrapper(this.dbName, 1);
    wrapper
      .configureStore('data', 'localId', true, [
        { name: 'serverId', keyPath: 'serverId' },
        { name: 'syncStatus', keyPath: 'syncStatus' },
      ])
      .configureStore('syncQueue', 'id', true, [
        { name: 'timestamp', keyPath: 'timestamp' },
      ]);

    await wrapper.open();
    this.db = wrapper.db;
    return this;
  }

  async saveLocal(data) {
    const record = {
      ...data,
      syncStatus: 'pending',
      localId: data.localId || crypto.randomUUID(),
      modifiedAt: new Date().toISOString(),
    };

    const store = this.db.transaction(['data', 'syncQueue'], 'readwrite');

    // Save data
    const dataStore = store.objectStore('data');
    await this._promisify(dataStore.put(record));

    // Add to sync queue
    const queueStore = store.objectStore('syncQueue');
    await this._promisify(
      queueStore.add({
        localId: record.localId,
        action: 'upsert',
        timestamp: Date.now(),
      })
    );

    return record.localId;
  }

  async getPendingSync() {
    const store = this.db
      .transaction(['data'], 'readonly')
      .objectStore('data')
      .index('syncStatus');

    return this._promisify(store.getAll('pending'));
  }

  async markSynced(localId, serverId) {
    const transaction = this.db.transaction(['data'], 'readwrite');
    const store = transaction.objectStore('data');

    const record = await this._promisify(store.get(localId));
    if (record) {
      record.serverId = serverId;
      record.syncStatus = 'synced';
      record.syncedAt = new Date().toISOString();
      await this._promisify(store.put(record));
    }
  }

  async sync(serverApi) {
    const pending = await this.getPendingSync();

    for (const record of pending) {
      try {
        const serverId = await serverApi.save(record);
        await this.markSynced(record.localId, serverId);
        console.log(`Synced: ${record.localId} -> ${serverId}`);
      } catch (error) {
        console.error(`Sync failed for ${record.localId}:`, error);
      }
    }
  }

  _promisify(request) {
    return new Promise((resolve, reject) => {
      request.onsuccess = () => resolve(request.result);
      request.onerror = () => reject(request.error);
    });
  }
}

// =============================================================================
// 9. Full Example Usage
// =============================================================================

async function runExamples() {
  console.log('=== IndexedDB Examples ===\n');

  // Using wrapper class
  const wrapper = new IndexedDBWrapper('exampleDB', 1);

  wrapper
    .configureStore('users', 'id', true, [
      { name: 'email', keyPath: 'email', options: { unique: true } },
      { name: 'name', keyPath: 'name' },
      { name: 'age', keyPath: 'age' },
    ])
    .configureStore('posts', 'id', true, [
      { name: 'authorId', keyPath: 'authorId' },
      { name: 'category', keyPath: 'category' },
    ]);

  await wrapper.open();
  console.log('Database opened\n');

  // Add users
  const users = wrapper.store('users');

  const user1Id = await users.add({
    name: 'Alice',
    email: 'alice@example.com',
    age: 28,
  });
  const user2Id = await users.add({
    name: 'Bob',
    email: 'bob@example.com',
    age: 35,
  });
  const user3Id = await users.add({
    name: 'Charlie',
    email: 'charlie@example.com',
    age: 22,
  });

  console.log('Added users:', { user1Id, user2Id, user3Id });

  // Get all users
  const allUsers = await users.getAll();
  console.log('All users:', allUsers);

  // Query by index
  const alice = await users.index('email').get('alice@example.com');
  console.log('Found Alice:', alice);

  // Update user
  await users.put({ ...alice, age: 29 });
  console.log('Updated Alice age to 29');

  // Count
  const count = await users.count();
  console.log('User count:', count);

  // Add posts
  const posts = wrapper.store('posts');
  await posts.add({
    title: 'Hello World',
    authorId: user1Id,
    category: 'tech',
  });
  await posts.add({
    title: 'JavaScript Tips',
    authorId: user1Id,
    category: 'tech',
  });
  await posts.add({
    title: 'Travel Guide',
    authorId: user2Id,
    category: 'travel',
  });

  // Get posts by author
  const alicePosts = await posts.index('authorId').getAll(user1Id);
  console.log('Alice posts:', alicePosts);

  // Cleanup
  wrapper.close();
  console.log('\nDatabase closed');
}

// Export for use in other files
if (typeof module !== 'undefined' && module.exports) {
  module.exports = {
    openDatabase,
    addRecord,
    getRecord,
    getAllRecords,
    updateRecord,
    deleteRecord,
    getByIndex,
    getAllByIndex,
    getInRange,
    IndexedDBWrapper,
    OfflineStore,
  };
}

// Run examples (comment out in production)
// runExamples().catch(console.error);
Examples - JavaScript Tutorial | DeepML