Trabaja con IndexedDB

En esta guía, se abordan los conceptos básicos de la API de IndexedDB. Usamos la biblioteca IndexedDB Promised de Jake Archibald, que es muy similar a la API de IndexedDB, pero usa promesas, que puedes usar con await para obtener una sintaxis más concisa. Esto simplifica la API y, al mismo tiempo, mantiene su estructura.

¿Qué es IndexedDB?

IndexedDB es un sistema de almacenamiento NoSQL a gran escala que permite almacenar casi cualquier elemento del navegador del usuario. Además de las acciones habituales de búsqueda, get y put, IndexedDB también admite transacciones y es muy adecuada para almacenar grandes cantidades de datos estructurados.

Cada base de datos de IndexedDB es única para un origen (por lo general, el dominio o subdominio del sitio), lo que significa que ningún otro origen puede acceder a ella. Sus límites de almacenamiento de datos suelen ser grandes, si existen, pero los distintos navegadores manejan los límites y la expulsión de datos de manera diferente. Consulta la sección Lecturas adicionales para obtener más información.

Términos de IndexedDB

Base de datos
Es el nivel más alto de IndexedDB. Contiene los almacenes de objetos que, a su vez, contienen los datos que deseas conservar. Puedes crear varias bases de datos con cualquier nombre que elijas.
Almacén de objetos
Un bucket individual para almacenar datos, similar a las tablas en bases de datos relacionales. Por lo general, hay un almacén de objetos para cada tipo (no de datos de JavaScript) de datos que almacenas. A diferencia de las tablas de base de datos, los tipos de datos de JavaScript en una tienda no necesitan ser coherentes. Por ejemplo, si una app tiene un almacén de objetos people que contiene información sobre tres personas, las propiedades de edad de esas personas podrían ser 53, 'twenty-five' y unknown.
Índice
Es un tipo de almacén de objetos para organizar datos en otro almacén de objetos (llamado almacén de objetos de referencia) según una propiedad individual de los datos. El índice se usa para recuperar registros en el almacén de objetos de esta propiedad. Por ejemplo, si almacenas personas, es posible que quieras buscarlas más tarde por su nombre, edad o animal favorito.
Operación
Una interacción con la base de datos.
Transacción
Wrapper alrededor de una operación o un grupo de operaciones que garantiza la integridad de la base de datos. Si una de las acciones de una transacción falla, ninguna de ellas se aplica y la base de datos vuelve al estado en el que estaba antes de que comenzara la transacción. Todas las operaciones de lectura o escritura en IndexedDB deben ser parte de una transacción. Esto permite operaciones atómicas de lectura, modificación y escritura sin el riesgo de generar conflictos con otros subprocesos que actúan en la base de datos al mismo tiempo.
Cursor
Es un mecanismo para iterar en varios registros de una base de datos.

Cómo comprobar la compatibilidad con IndexedDB

IndexedDB es casi compatible a nivel universal. Sin embargo, si estás trabajando con navegadores más antiguos, no es una mala idea detectar funciones por si acaso. La forma más sencilla es verificar el objeto window:

function indexedDBStuff () {
  // Check for IndexedDB support:
  if (!('indexedDB' in window)) {
    // Can't use IndexedDB
    console.log("This browser doesn't support IndexedDB");
    return;
  } else {
    // Do IndexedDB stuff here:
    // ...
  }
}

// Run IndexedDB code:
indexedDBStuff();

Cómo abrir una base de datos

Con IndexedDB, puedes crear múltiples bases de datos con el nombre que elijas. Si una base de datos no existe cuando intentas abrirla, se crea automáticamente. Para abrir una base de datos, usa el método openDB() de la biblioteca idb:

import {openDB} from 'idb';

async function useDB () {
  // Returns a promise, which makes `idb` usable with async-await.
  const dbPromise = await openDB('example-database', version, events);
}

useDB();

Este método muestra una promesa que se resuelve en un objeto de base de datos. Cuando uses el método openDB(), proporciona un nombre, un número de versión y un objeto de eventos para configurar la base de datos.

Este es un ejemplo del método openDB() en contexto:

import {openDB} from 'idb';

async function useDB () {
  // Opens the first version of the 'test-db1' database.
  // If the database does not exist, it will be created.
  const dbPromise = await openDB('test-db1', 1);
}

useDB();

Coloca la verificación de compatibilidad con IndexedDB en la parte superior de la función anónima. Esta acción cierra la función si el navegador no admite IndexedDB. Si la función puede continuar, llama al método openDB() para abrir una base de datos llamada 'test-db1'. En este ejemplo, se omitió el objeto de eventos opcionales para simplificar el proceso, pero debes especificarlo si deseas realizar cualquier trabajo significativo con IndexedDB.

Cómo trabajar con almacenes de objetos

Una base de datos IndexedDB contiene uno o más almacenes de objetos, que tienen una columna para una clave y otra para los datos asociados con esa clave.

Crea almacenes de objetos

Una base de datos IndexedDB bien estructurada debe tener un almacén de objetos para cada tipo de datos que se deban conservar. Por ejemplo, un sitio que conserva perfiles y notas de usuario podría tener un almacén de objetos people que contenga objetos person y un almacén de objetos notes que contenga objetos note.

Para garantizar la integridad de la base de datos, solo puedes crear o quitar almacenes de objetos en el objeto de eventos en una llamada a openDB(). El objeto de eventos expone un método upgrade() que te permite crear almacenes de objetos. Llama al método createObjectStore() dentro del método upgrade() para crear el almacén de objetos:

import {openDB} from 'idb';

async function createStoreInDB () {
  const dbPromise = await openDB('example-database', 1, {
    upgrade (db) {
      // Creates an object store:
      db.createObjectStore('storeName', options);
    }
  });
}

createStoreInDB();

Este método toma el nombre del almacén de objetos y un objeto de configuración opcional que te permite definir varias propiedades para el almacén de objetos.

El siguiente es un ejemplo de cómo usar createObjectStore():

import {openDB} from 'idb';

async function createStoreInDB () {
  const dbPromise = await openDB('test-db1', 1, {
    upgrade (db) {
      console.log('Creating a new object store...');

      // Checks if the object store exists:
      if (!db.objectStoreNames.contains('people')) {
        // If the object store does not exist, create it:
        db.createObjectStore('people');
      }
    }
  });
}

createStoreInDB();

En este ejemplo, un objeto de eventos se pasa al método openDB() para crear el almacén de objetos y, como antes, el trabajo de creación del almacén de objetos se realiza en el método upgrade() del objeto de evento. Sin embargo, debido a que el navegador muestra un error si intentas crear un almacén de objetos que ya existe, te recomendamos unir el método createObjectStore() en una sentencia if que verifique si el almacén de objetos existe. Dentro del bloque if, llama a createObjectStore() para crear un almacén de objetos llamado 'firstOS'.

Cómo definir claves primarias

Cuando defines almacenes de objetos, puedes definir cómo se identifican los datos de manera inequívoca en el almacén con una clave primaria. Para definir una clave primaria, puedes definir una ruta de acceso de la clave o usar un generador de claves.

Una ruta de acceso de la clave es una propiedad que siempre existe y contiene un valor único. Por ejemplo, en el caso de un almacén de objetos people, puedes elegir la dirección de correo electrónico como la ruta de acceso de la clave:

import {openDB} from 'idb';

async function createStoreInDB () {
  const dbPromise = await openDB('test-db2', 1, {
    upgrade (db) {
      if (!db.objectStoreNames.contains('people')) {
        db.createObjectStore('people', { keyPath: 'email' });
      }
    }
  });
}

createStoreInDB();

En este ejemplo, se crea un almacén de objetos llamado 'people' y se asigna la propiedad email como clave primaria en la opción keyPath.

También puedes usar un generador de claves, como autoIncrement. El generador de claves crea un valor único para cada objeto agregado al almacén de objetos. De forma predeterminada, si no especificas una clave, IndexedDB crea una y la almacena por separado de los datos.

En el siguiente ejemplo, se crea un almacén de objetos llamado 'notes' y se configura la clave primaria para que se asigne automáticamente como un número de incremento automático:

import {openDB} from 'idb';

async function createStoreInDB () {
  const dbPromise = await openDB('test-db2', 1, {
    upgrade (db) {
      if (!db.objectStoreNames.contains('notes')) {
        db.createObjectStore('notes', { autoIncrement: true });
      }
    }
  });
}

createStoreInDB();

El siguiente ejemplo es similar al anterior, pero esta vez el valor de incremento automático se asigna de forma explícita a una propiedad llamada 'id'.

import {openDB} from 'idb';

async function createStoreInDB () {
  const dbPromise = await openDB('test-db2', 1, {
    upgrade (db) {
      if (!db.objectStoreNames.contains('logs')) {
        db.createObjectStore('logs', { keyPath: 'id', autoIncrement: true });
      }
    }
  });
}

createStoreInDB();

Elegir qué método usar para definir la clave depende de tus datos. Si tus datos tienen una propiedad que siempre es única, puedes convertirla en la keyPath para aplicarla. De lo contrario, usa un valor de incremento automático.

El siguiente código crea tres almacenes de objetos que demuestran las distintas formas de definir claves primarias en los almacenes de objetos:

import {openDB} from 'idb';

async function createStoresInDB () {
  const dbPromise = await openDB('test-db2', 1, {
    upgrade (db) {
      if (!db.objectStoreNames.contains('people')) {
        db.createObjectStore('people', { keyPath: 'email' });
      }

      if (!db.objectStoreNames.contains('notes')) {
        db.createObjectStore('notes', { autoIncrement: true });
      }

      if (!db.objectStoreNames.contains('logs')) {
        db.createObjectStore('logs', { keyPath: 'id', autoIncrement: true });
      }
    }
  });
}

createStoresInDB();

Cómo definir índices

Los índices son un tipo de almacén de objetos que se usa para recuperar datos del almacén de objetos de referencia según una propiedad especificada. Un índice se encuentra dentro del almacén de objetos de referencia y contiene los mismos datos, pero usa la propiedad especificada como su ruta de acceso de la clave en lugar de la clave primaria del almacén de referencia. Los índices se deben generar cuando creas tus almacenes de objetos y se pueden usar para definir una restricción única en los datos.

Para crear un índice, llama al método createIndex() en una instancia de almacén de objetos:

import {openDB} from 'idb';

async function createIndexInStore() {
  const dbPromise = await openDB('storeName', 1, {
    upgrade (db) {
      const objectStore = db.createObjectStore('storeName');

      objectStore.createIndex('indexName', 'property', options);
    }
  });
}

createIndexInStore();

Este método crea y muestra un objeto de índice. El método createIndex() de la instancia del almacén de objetos toma el nombre del índice nuevo como primer argumento, y el segundo se refiere a la propiedad de los datos que deseas indexar. El argumento final te permite definir dos opciones que determinan cómo funciona el índice: unique y multiEntry. Si unique se configura como true, el índice no permite valores duplicados para una sola clave. A continuación, multiEntry determina el comportamiento de createIndex() cuando la propiedad indexada es un array. Si se configura como true, createIndex() agrega una entrada en el índice para cada elemento del array. De lo contrario, se agrega una sola entrada que contiene el array.

Por ejemplo:

import {openDB} from 'idb';

async function createIndexesInStores () {
  const dbPromise = await openDB('test-db3', 1, {
    upgrade (db) {
      if (!db.objectStoreNames.contains('people')) {
        const peopleObjectStore = db.createObjectStore('people', { keyPath: 'email' });

        peopleObjectStore.createIndex('gender', 'gender', { unique: false });
        peopleObjectStore.createIndex('ssn', 'ssn', { unique: true });
      }

      if (!db.objectStoreNames.contains('notes')) {
        const notesObjectStore = db.createObjectStore('notes', { autoIncrement: true });

        notesObjectStore.createIndex('title', 'title', { unique: false });
      }

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

createIndexesInStores();

En este ejemplo, los almacenes de objetos 'people' y 'notes' tienen índices. Para crear los índices, primero asigna el resultado de createObjectStore() (un objeto de almacenamiento de objetos) a una variable para poder llamar a createIndex() en ella.

Cómo trabajar con datos

En esta sección, se describe cómo crear, leer, actualizar y borrar datos. Todas estas operaciones son asíncronas, con promesas en las que la API de IndexedDB usa solicitudes. Esto simplifica la API. En lugar de escuchar eventos activados por la solicitud, puedes llamar a .then() en el objeto de base de datos que muestra el método openDB() para iniciar interacciones con la base de datos o await su creación.

Todas las operaciones de datos de IndexedDB se llevan a cabo dentro de una transacción. Cada operación tiene el siguiente formato:

  1. Obtener objeto de base de datos
  2. Transacción abierta en la base de datos.
  3. Abrir almacén de objetos en la transacción.
  4. Realizar la operación en el almacén de objetos

Una transacción se puede considerar como un wrapper seguro alrededor de una operación o un grupo de operaciones. Si una de las acciones dentro de una transacción falla, todas las acciones se revierten. Las transacciones son específicas de uno o más almacenes de objetos, que defines cuando abres la transacción. Pueden ser de solo lectura o de lectura y escritura. Esto indica si las operaciones dentro de la transacción leen los datos o realizan un cambio en la base de datos.

Crea datos

Para crear datos, llama al método add() en la instancia de la base de datos y pasa los datos que desees agregar. El primer argumento del método add() es el almacén de objetos al que deseas agregar los datos, y el segundo argumento es un objeto que contiene los campos y los datos asociados que deseas agregar. Este es el ejemplo más simple, donde se agrega una sola fila de datos:

import {openDB} from 'idb';

async function addItemToStore () {
  const db = await openDB('example-database', 1);

  await db.add('storeName', {
    field: 'data'
  });
}

addItemToStore();

Cada llamada a add() ocurre dentro de una transacción, por lo que, incluso si la promesa se resuelve de forma correcta, no necesariamente significa que la operación funcionó. Para asegurarte de que se realizó la operación de adición, debes verificar si se completó toda la transacción con el método transaction.done(). Esta es una promesa que se resuelve cuando la transacción se completa sola y se rechaza si la transacción falla. Debes realizar esta verificación para todas las operaciones de "escritura", ya que es la única manera de saber que los cambios en la base de datos realmente ocurrieron.

En el siguiente código, se muestra el uso del método add() dentro de una transacción:

import {openDB} from 'idb';

async function addItemsToStore () {
  const db = await openDB('test-db4', 1, {
    upgrade (db) {
      if (!db.objectStoreNames.contains('foods')) {
        db.createObjectStore('foods', { keyPath: 'name' });
      }
    }
  });
  
  // Create a transaction on the 'foods' store in read/write mode:
  const tx = db.transaction('foods', 'readwrite');

  // Add multiple items to the 'foods' store in a single transaction:
  await Promise.all([
    tx.store.add({
      name: 'Sandwich',
      price: 4.99,
      description: 'A very tasty sandwich!',
      created: new Date().getTime(),
    }),
    tx.store.add({
      name: 'Eggs',
      price: 2.99,
      description: 'Some nice eggs you can cook up!',
      created: new Date().getTime(),
    }),
    tx.done
  ]);
}

addItemsToStore();

Una vez que abras la base de datos (y crees un almacén de objetos, si es necesario), deberás abrir una transacción llamando al método transaction() en ella. Este método toma un argumento para la tienda en la que quieres realizar la transacción y el modo. En este caso, nos interesa escribir en la tienda, por lo que en este ejemplo se especifica 'readwrite'.

El siguiente paso es comenzar a agregar artículos a la tienda como parte de la transacción. En el ejemplo anterior, se trata de tres operaciones en el almacén 'foods', en las que cada una muestra una promesa:

  1. Agregando la grabación de un sándwich sabroso.
  2. Se agregará un registro para algunos huevos.
  3. Indica que se completó la transacción (tx.done).

Debido a que todas estas acciones se basan en promesas, debemos esperar a que todas finalicen. Pasar estas promesas a Promise.all es una forma agradable y ergonómica de hacerlo. Promise.all acepta un array de promesas y finaliza cuando se resuelven todas las promesas que se le pasaron.

En el caso de los dos registros que se agregan, la interfaz store de la instancia de transacción llama a add() y le pasa los datos. Puedes usar await en la llamada a Promise.all para que finalice cuando se complete la transacción.

Cómo leer datos

Para leer datos, llama al método get() en la instancia de base de datos que recuperas con el método openDB(). get() toma el nombre del almacén y el valor de la clave primaria del objeto que deseas recuperar. Aquí hay un ejemplo básico:

import {openDB} from 'idb';

async function getItemFromStore () {
  const db = await openDB('example-database', 1);

  // Get a value from the object store by its primary key value:
  const value = await db.get('storeName', 'unique-primary-key-value');
}

getItemFromStore();

Al igual que con add(), el método get() muestra una promesa, por lo que puedes usar await si lo prefieres o usar la devolución de llamada .then() de la promesa.

En el siguiente ejemplo, se usa el método get() en el almacén de objetos 'foods' de la base de datos 'test-db4' para obtener una sola fila con la clave primaria 'name':

import {openDB} from 'idb';

async function getItemFromStore () {
  const db = await openDB('test-db4', 1);
  const value = await db.get('foods', 'Sandwich');

  console.dir(value);
}

getItemFromStore();

Recuperar una sola fila de la base de datos es bastante sencillo: abre la base de datos y especifica el almacén de objetos y el valor de la clave primaria de la fila de la que deseas obtener datos. Como el método get() muestra una promesa, puedes usar await.

Cómo actualizar datos

Para actualizar datos, llama al método put() en el almacén de objetos. El método put() es similar al método add() y también se puede usar en lugar de add() para crear datos. A continuación, se muestra un ejemplo básico del uso de put() para actualizar una fila en un almacén de objetos según su valor de clave primaria:

import {openDB} from 'idb';

async function updateItemInStore () {
  const db = await openDB('example-database', 1);

  // Update a value from in an object store with an inline key:
  await db.put('storeName', { inlineKeyName: 'newValue' });

  // Update a value from in an object store with an out-of-line key.
  // In this case, the out-of-line key value is 1, which is the
  // auto-incremented value.
  await db.put('otherStoreName', { field: 'value' }, 1);
}

updateItemInStore();

Al igual que otros métodos, este método muestra una promesa. También puedes usar put() como parte de una transacción. El siguiente es un ejemplo con la tienda 'foods' anterior que actualiza el precio del sándwich y los huevos:

import {openDB} from 'idb';

async function updateItemsInStore () {
  const db = await openDB('test-db4', 1);
  
  // Create a transaction on the 'foods' store in read/write mode:
  const tx = db.transaction('foods', 'readwrite');

  // Update multiple items in the 'foods' store in a single transaction:
  await Promise.all([
    tx.store.put({
      name: 'Sandwich',
      price: 5.99,
      description: 'A MORE tasty sandwich!',
      updated: new Date().getTime() // This creates a new field
    }),
    tx.store.put({
      name: 'Eggs',
      price: 3.99,
      description: 'Some even NICER eggs you can cook up!',
      updated: new Date().getTime() // This creates a new field
    }),
    tx.done
  ]);
}

updateItemsInStore();

La forma en que se actualizan los elementos depende de cómo configures una clave. Si configuras una keyPath, cada fila en el almacén de objetos se asocia con una clave intercalada. En el ejemplo anterior, se actualizan las filas en función de esta clave y, cuando lo hagas, deberás especificar esa clave para actualizar el elemento adecuado en el almacén de objetos. También puedes crear una clave fuera de línea si configuras una autoIncrement como clave primaria.

Borra datos

Para borrar datos, llama al método delete() en el almacén de objetos:

import {openDB} from 'idb';

async function deleteItemFromStore () {
  const db = await openDB('example-database', 1);

  // Delete a value 
  await db.delete('storeName', 'primary-key-value');
}

deleteItemFromStore();

Al igual que con add() y put(), puedes usar esto como parte de una transacción:

import {openDB} from 'idb';

async function deleteItemsFromStore () {
  const db = await openDB('test-db4', 1);
  
  // Create a transaction on the 'foods' store in read/write mode:
  const tx = db.transaction('foods', 'readwrite');

  // Delete multiple items from the 'foods' store in a single transaction:
  await Promise.all([
    tx.store.delete('Sandwich'),
    tx.store.delete('Eggs'),
    tx.done
  ]);
}

deleteItemsFromStore();

La estructura de la interacción de la base de datos es la misma que la de las otras operaciones. Para verificar que se haya completado toda la transacción, incluye el método tx.done en el array que pasas a Promise.all.

Cómo obtener todos los datos

Hasta ahora, solo recuperaste objetos de la tienda uno a la vez. También puedes recuperar todos los datos, o un subconjunto, de un almacén de objetos o índice con el método getAll() o los cursores.

El método getAll()

La forma más sencilla de recuperar todos los datos de un almacén de objetos es llamar a getAll() en el almacén o el índice de objetos, de la siguiente manera:

import {openDB} from 'idb';

async function getAllItemsFromStore () {
  const db = await openDB('test-db4', 1);

  // Get all values from the designated object store:
  const allValues = await db.getAll('storeName');

  console.dir(allValues);
}

getAllItemsFromStore();

Este método muestra todos los objetos en el almacén de objetos, sin restricciones de ningún tipo. Es la forma más directa de obtener todos los valores de un almacén de objetos, pero también la menos flexible.

import {openDB} from 'idb';

async function getAllItemsFromStore () {
  const db = await openDB('test-db4', 1);

  // Get all values from the designated object store:
  const allValues = await db.getAll('foods');

  console.dir(allValues);
}

getAllItemsFromStore();

En este ejemplo, se llama a getAll() en el almacén de objetos 'foods'. Esto muestra todos los objetos de 'foods', ordenados según la clave primaria.

Cómo usar cursores

Los cursores son una forma más flexible de recuperar varios objetos. El cursor selecciona cada objeto de un almacén de objetos o de índice uno por uno, lo que te permite hacer algo con los datos cuando están seleccionados. Los cursores, como las otras operaciones de la base de datos, funcionan en transacciones.

Para crear un cursor, llama a openCursor() en el almacén de objetos como parte de una transacción. Con el almacén 'foods' de ejemplos anteriores, esta es la forma de hacer avanzar un cursor por todas las filas de datos en un almacén de objetos:

import {openDB} from 'idb';

async function getAllItemsFromStoreWithCursor () {
  const db = await openDB('test-db4', 1);
  const tx = await db.transaction('foods', 'readonly');

  // Open a cursor on the designated object store:
  let cursor = await tx.store.openCursor();

  // Iterate on the cursor, row by row:
  while (cursor) {
    // Show the data in the row at the current cursor position:
    console.log(cursor.key, cursor.value);

    // Advance the cursor to the next row:
    cursor = await cursor.continue();
  }
}

getAllItemsFromStoreWithCursor();

En este caso, la transacción se abre en modo 'readonly' y se llama al método openCursor. En un bucle while posterior, la fila en la posición actual del cursor puede tener sus propiedades key y value leídas, y puedes operar sobre esos valores de la manera que tenga más sentido para tu app. Cuando esté todo listo, puedes llamar al método continue() del objeto cursor para ir a la siguiente fila, y el bucle while finaliza cuando el cursor llega al final del conjunto de datos.

Usa cursores con índices y rangos

Los índices te permiten recuperar los datos en un almacén de objetos mediante una propiedad que no sea la clave primaria. Puedes crear un índice en cualquier propiedad, que se convierte en la keyPath del índice, especificar un rango en esa propiedad y obtener los datos dentro del rango con getAll() o un cursor.

Define tu rango con el objeto IDBKeyRange y cualquiera de los siguientes métodos:

Los métodos upperBound() y lowerBound() especifican los límites inferiores y superiores del rango.

IDBKeyRange.lowerBound(indexKey);

o:

IDBKeyRange.upperBound(indexKey);

Cada una toma un argumento: el valor keyPath del índice para el elemento que deseas especificar como límite superior o inferior.

El método bound() especifica un límite inferior y superior:

IDBKeyRange.bound(lowerIndexKey, upperIndexKey);

El rango de estas funciones es inclusivo de forma predeterminada, lo que significa que incluye los datos que se especifican como los límites del rango. Si quieres omitir esos valores, especifica el rango como exclusivo. Para ello, pasa true como el segundo argumento de lowerBound() o upperBound(), o como tercer y cuarto argumento de bound() para los límites inferior y superior, respectivamente.

En el siguiente ejemplo, se usa un índice en la propiedad 'price' del almacén de objetos 'foods'. El almacén ahora también tiene un formulario adjunto con dos entradas para los límites superior e inferior del rango. Usa el siguiente código para encontrar alimentos con precios entre esos límites:

import {openDB} from 'idb';

async function searchItems (lower, upper) {
  if (!lower === '' && upper === '') {
    return;
  }

  let range;

  if (lower !== '' && upper !== '') {
    range = IDBKeyRange.bound(lower, upper);
  } else if (lower === '') {
    range = IDBKeyRange.upperBound(upper);
  } else {
    range = IDBKeyRange.lowerBound(lower);
  }

  const db = await openDB('test-db4', 1);
  const tx = await db.transaction('foods', 'readonly');
  const index = tx.store.index('price');

  // Open a cursor on the designated object store:
  let cursor = await index.openCursor(range);

  if (!cursor) {
    return;
  }

  // Iterate on the cursor, row by row:
  while (cursor) {
    // Show the data in the row at the current cursor position:
    console.log(cursor.key, cursor.value);

    // Advance the cursor to the next row:
    cursor = await cursor.continue();
  }
}

// Get items priced between one and four dollars:
searchItems(1.00, 4.00);

En el código de ejemplo, primero se obtienen los valores de los límites y se verifica si estos existen. El siguiente bloque de código decide qué método usar para limitar el rango en función de los valores. En la interacción de la base de datos, abre el almacén de objetos en la transacción como de costumbre y, luego, abre el índice 'price' en el almacén de objetos. El índice 'price' te permite buscar elementos por precio.

Luego, el código abre un cursor en el índice y pasa el rango. El cursor muestra una promesa que representa el primer objeto del rango, o undefined si no hay datos en el rango. El método cursor.continue() muestra un cursor que representa el siguiente objeto y continúa a través del bucle hasta llegar al final del rango.

Control de versiones de la base de datos

Cuando llamas al método openDB(), puedes especificar el número de versión de la base de datos en el segundo parámetro. En todos los ejemplos de esta guía, la versión se estableció como 1, pero una base de datos se puede actualizar a una versión nueva si necesitas modificarla de alguna manera. Si la versión especificada es superior a la versión de la base de datos existente, se ejecutará la devolución de llamada upgrade en el objeto de evento, lo que te permitirá agregar índices y almacenes de objetos nuevos a la base de datos.

El objeto db de la devolución de llamada upgrade tiene una propiedad oldVersion especial, que indica el número de versión de la base de datos a la que tiene acceso el navegador. Puedes pasar este número de versión a una instrucción switch para ejecutar bloques de código dentro de la devolución de llamada upgrade en función del número de versión de la base de datos existente. Por ejemplo:

import {openDB} from 'idb';

const db = await openDB('example-database', 2, {
  upgrade (db, oldVersion) {
    switch (oldVersion) {
      case 0:
        // Create first object store:
        db.createObjectStore('store', { keyPath: 'name' });

      case 1:
        // Get the original object store, and create an index on it:
        const tx = await db.transaction('store', 'readwrite');
        tx.store.createIndex('name', 'name');
    }
  }
});

En este ejemplo, se establece la versión más reciente de la base de datos en 2. Cuando se ejecuta este código por primera vez, la base de datos aún no existe en el navegador, por lo que oldVersion es 0, y la declaración switch comienza en case 0. En el ejemplo, se agrega un almacén de objetos 'store' a la base de datos.

Punto clave: En las declaraciones switch, por lo general, hay un break después de cada bloque case, pero esto no se usa deliberadamente aquí. De esta manera, si la base de datos existente tiene algunas versiones atrasadas o si no existe, el código continúa con el resto de los bloques case hasta que esté actualizado. Por lo tanto, en el ejemplo, el navegador sigue ejecutándose a través de case 1 y crea un índice name en el almacén de objetos store.

Para crear un índice 'description' en el almacén de objetos 'store', actualiza el número de versión y agrega un nuevo bloque case de la siguiente manera:

import {openDB} from 'idb';

const db = await openDB('example-database', 3, {
  upgrade (db, oldVersion) {
    switch (oldVersion) {
      case 0:
        // Create first object store:
        db.createObjectStore('store', { keyPath: 'name' });

      case 1:
        // Get the original object store, and create an index on it:
        const tx = await db.transaction('store', 'readwrite');
        tx.store.createIndex('name', 'name');

      case 2:
        const tx = await db.transaction('store', 'readwrite');
        tx.store.createIndex('description', 'description');
    }
  }
});

Si la base de datos que creaste en el ejemplo anterior aún existe en el navegador, cuando se ejecuta, oldVersion es 2. El navegador omite case 0 y case 1, y ejecuta el código en case 2, que crea un índice description. Luego, el navegador tiene una base de datos en la versión 3 que contiene un almacén de objetos store con los índices name y description.

Lecturas adicionales

En los siguientes recursos, se proporciona más información y contexto para usar IndexedDB.

Documentación de IndexedDB

Límites de almacenamiento de datos