Trabalhar com IndexedDB

Este guia aborda os conceitos básicos da API IndexedDB. Estamos usando a biblioteca IndexedDB Promised de Jake Archibald, que é muito semelhante à API IndexedDB, mas usa promessas, que podem ser await para uma sintaxe mais concisa. Isso simplifica a API, mas mantém a estrutura dela.

O que é o IndexedDB?

O IndexedDB é um sistema de armazenamento NoSQL em grande escala que permite armazenar quase tudo no navegador do usuário. Além das ações comuns de pesquisa, recebimento e colocação, o IndexedDB também oferece suporte a transações e é adequado para armazenar grandes quantidades de dados estruturados.

Cada banco de dados do IndexedDB é exclusivo de uma origem (geralmente o domínio ou subdomínio do site), o que significa que ele não pode acessar nem ser acessado por nenhuma outra origem. Os limites de armazenamento de dados geralmente são grandes, se existirem, mas navegadores diferentes lidam com limites e remoção de dados de maneira diferente. Consulte a seção Leitura adicional para mais informações.

Termos do IndexedDB

Banco de dados
O nível mais alto do IndexedDB. Ele contém os repositórios de objetos, que, por sua vez, contêm os dados que você quer manter. É possível criar vários bancos de dados com os nomes que você escolher.
Repositório de objetos
Um bucket individual para armazenar dados, semelhante a tabelas em bancos de dados relacionais. Normalmente, há um repositório de objetos para cada tipo (não o tipo de dados do JavaScript) de dados que você está armazenando. Ao contrário das tabelas de banco de dados, os tipos de dados JavaScript em um repositório não precisam ser consistentes. Por exemplo, se um app tiver um repositório de objetos people com informações sobre três pessoas, as propriedades de idade dessas pessoas poderão ser 53, 'twenty-five' e unknown.
Índice
Um tipo de repositório de objetos para organizar dados em outro repositório de objetos (chamado de repositório de objetos de referência) por uma propriedade individual dos dados. O índice é usado para recuperar registros no repositório de objetos por essa propriedade. Por exemplo, se você estiver armazenando pessoas, talvez queira buscá-las mais tarde pelo nome, idade ou animal favorito.
Operação
Uma interação com o banco de dados.
Transação
Um wrapper em torno de uma operação ou grupo de operações que garante a integridade do banco de dados. Se uma das ações em uma transação falhar, nenhuma delas será aplicada, e o banco de dados retornará ao estado em que estava antes do início da transação. Todas as operações de leitura ou gravação no IndexedDB precisam fazer parte de uma transação. Isso permite operações atômicas de leitura, modificação e gravação sem o risco de conflitos com outras linhas de execução que atuam no banco de dados ao mesmo tempo.
Cursor
Um mecanismo para iterar em vários registros em um banco de dados.

Como verificar se há suporte para IndexedDB

O IndexedDB é compatível com quase todos os navegadores. No entanto, se você estiver trabalhando com navegadores mais antigos, é uma boa ideia detectar o suporte a recursos, por precaução. A maneira mais fácil é verificar o 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();

Como abrir um banco de dados

Com o IndexedDB, é possível criar vários bancos de dados com os nomes que você escolher. Se um banco de dados não existir quando você tentar abri-lo, ele será criado automaticamente. Para abrir um banco de dados, use o método openDB() da 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();

Esse método retorna uma promessa que é resolvida em um objeto de banco de dados. Ao usar o método openDB(), forneça um nome, um número de versão e um objeto de eventos para configurar o banco de dados.

Confira um exemplo do método openDB() no 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();

Coloque a verificação de suporte ao IndexedDB na parte de cima da função anônima. Isso sai da função se o navegador não for compatível com o IndexedDB. Se a função puder continuar, ela vai chamar o método openDB() para abrir um banco de dados chamado 'test-db1'. Neste exemplo, o objeto de eventos opcional foi deixado de fora para simplificar, mas você precisa especificá-lo para fazer qualquer trabalho significativo com o IndexedDB.

Como trabalhar com repositórios de objetos

Um banco de dados IndexedDB contém um ou mais repositórios de objetos, cada um com uma coluna para uma chave e outra para os dados associados a essa chave.

Criar repositórios de objetos

Um banco de dados IndexedDB bem estruturado precisa ter um repositório de objetos para cada tipo de dados que precisa ser mantido. Por exemplo, um site que mantém perfis de usuário e notas pode ter um repositório de objetos people contendo objetos person e um repositório de objetos notes contendo objetos note.

Para garantir a integridade do banco de dados, só é possível criar ou remover repositórios de objetos no objeto de eventos em uma chamada openDB(). O objeto de eventos expõe um método upgrade() que permite criar repositórios de objetos. Chame o método createObjectStore() dentro do método upgrade() para criar o repositório 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();

Esse método usa o nome do repositório de objetos e um objeto de configuração opcional que permite definir várias propriedades para o repositório de objetos.

Confira um exemplo de como 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();

Neste exemplo, um objeto de eventos é transmitido ao método openDB() para criar o repositório de objetos. Como antes, o trabalho de criação do repositório de objetos é feito no método upgrade() do objeto de evento. No entanto, como o navegador gera um erro se você tentar criar um repositório de objetos que já existe, recomendamos encapsular o método createObjectStore() em uma instrução if que verifique se o repositório de objetos existe. No bloco if, chame createObjectStore() para criar um repositório de objetos chamado 'firstOS'.

Como definir chaves primárias

Ao definir repositórios de objetos, é possível definir como os dados são identificados de forma exclusiva no repositório usando uma chave primária. É possível definir uma chave primária definindo um caminho de chave ou usando um gerador de chaves.

Um caminho de chave é uma propriedade que sempre existe e contém um valor exclusivo. Por exemplo, no caso de um repositório de objetos people, você pode escolher o endereço de e-mail como o caminho da chave:

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();

Este exemplo cria um repositório de objetos chamado 'people' e atribui a propriedade email como a chave primária na opção keyPath.

Também é possível usar um gerador de chaves, como autoIncrement. O gerador de chaves cria um valor exclusivo para cada objeto adicionado ao repositório de objetos. Por padrão, se você não especificar uma chave, o IndexedDB vai criar uma e armazená-la separadamente dos dados.

O exemplo a seguir cria um repositório de objetos chamado 'notes' e define a chave primária para ser atribuída automaticamente como um 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();

O exemplo a seguir é semelhante ao anterior, mas, desta vez, o valor de incremento automático é explicitamente atribuído a uma propriedade chamada '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();

A escolha do método para definir a chave depende dos seus dados. Se os dados tiverem uma propriedade que é sempre exclusiva, você poderá torná-la o keyPath para aplicar essa exclusividade. Caso contrário, use um valor de incremento automático.

O código a seguir cria três repositórios de objetos, demonstrando as várias maneiras de definir chaves primárias neles:

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();

Como definir índices

Os índices são um tipo de repositório de objetos usado para recuperar dados do repositório de objetos de referência por uma propriedade especificada. Um índice reside no repositório de objetos de referência e contém os mesmos dados, mas usa a propriedade especificada como o caminho da chave em vez da chave primária do repositório de referência. Os índices precisam ser criados quando você cria os repositórios de objetos e podem ser usados para definir uma restrição exclusiva nos dados.

Para criar um índice, chame o método createIndex() em uma instância de repositório 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();

Esse método cria e retorna um objeto de índice. O método createIndex() na instância do repositório de objetos usa o nome do novo índice como o primeiro argumento, e o segundo argumento se refere à propriedade nos dados que você quer indexar. O argumento final permite definir duas opções que determinam como o índice opera: unique e multiEntry. Se unique for definido como true, o índice não vai permitir valores duplicados para uma única chave. Em seguida, multiEntry determina como createIndex() se comporta quando a propriedade indexada é uma matriz. Se definido como true, o createIndex() adiciona uma entrada no índice para cada elemento da matriz. Caso contrário, ela adiciona uma única entrada contendo a matriz.

Veja um exemplo:

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();

Neste exemplo, os repositórios de objetos 'people' e 'notes' têm índices. Para criar os índices, primeiro atribua o resultado de createObjectStore() (um objeto de repositório de objetos) a uma variável para que você possa chamar createIndex() nele.

Como trabalhar com dados

Nesta seção, descrevemos como criar, ler, atualizar e excluir dados. Todas essas operações são assíncronas, usando promessas em que a API IndexedDB usa solicitações. Isso simplifica a API. Em vez de detectar eventos acionados pela solicitação, é possível chamar .then() no objeto de banco de dados retornado do método openDB() para iniciar interações com o banco de dados ou await a criação dele.

Todas as operações de dados no IndexedDB são realizadas dentro de uma transação. Cada operação tem o seguinte formato:

  1. Receber objeto de banco de dados.
  2. Abra a transação no banco de dados.
  3. Abra o repositório de objetos na transação.
  4. Realizar operação no repositório de objetos.

Uma transação pode ser considerada um wrapper seguro em torno de uma operação ou grupo de operações. Se uma das ações em uma transação falhar, todas as ações serão revertidas. As transações são específicas para um ou mais repositórios de objetos, que você define ao abrir a transação. Eles podem ser somente leitura ou leitura e gravação. Isso indica se as operações dentro da transação leem os dados ou fazem uma alteração no banco de dados.

Criar dados

Para criar dados, chame o método add() na instância do banco de dados e transmita os dados que você quer adicionar. O primeiro argumento do método add() é o repositório de objetos em que você quer adicionar os dados, e o segundo é um objeto que contém os campos e os dados associados que você quer adicionar. Veja o exemplo mais simples, em que uma única linha de dados é adicionada:

import {openDB} from 'idb';

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

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

addItemToStore();

Cada chamada add() acontece em uma transação. Portanto, mesmo que a promessa seja resolvida, isso não significa necessariamente que a operação funcionou. Para garantir que a operação de adição foi realizada, verifique se toda a transação foi concluída usando o método transaction.done(). Essa é uma promessa que é resolvida quando a transação é concluída e rejeitada se houver erros na transação. É preciso realizar essa verificação para todas as operações de "gravação", porque essa é a única maneira de saber se as mudanças no banco de dados realmente ocorreram.

O código a seguir mostra o uso do método add() dentro de uma transação:

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();

Depois de abrir o banco de dados (e criar um repositório de objetos, se necessário), você precisará abrir uma transação chamando o método transaction() nele. Esse método usa um argumento para a loja em que você quer fazer a transação, além do modo. Nesse caso, queremos gravar no repositório, então este exemplo especifica 'readwrite'.

A próxima etapa é começar a adicionar itens à loja como parte da transação. No exemplo anterior, estamos lidando com três operações no armazenamento 'foods', cada uma retornando uma promessa:

  1. Adicionando um registro para um sanduíche saboroso.
  2. Adicionando um registro para ovos.
  3. Sinalização de que a transação foi concluída (tx.done).

Como todas essas ações são baseadas em promessas, precisamos esperar que todas elas sejam concluídas. Transmitir essas promessas para Promise.all é uma maneira prática e ergonômica de fazer isso. Promise.all aceita uma matriz de promessas e termina quando todas as promessas transmitidas a ela são resolvidas.

Para os dois registros que estão sendo adicionados, a interface store da instância de transação chama add() e transmite os dados para ela. É possível await a chamada Promise.all para que ela seja encerrada quando a transação for concluída.

Ler dados

Para ler dados, chame o método get() na instância do banco de dados que você recupera usando o método openDB(). get() usa o nome do armazenamento e o valor da chave primária do objeto que você quer recuperar. Confira um exemplo 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();

Assim como em add(), o método get() retorna uma promessa. Portanto, é possível usar await se preferir ou o callback .then() da promessa.

O exemplo a seguir usa o método get() no repositório de objetos 'foods' do banco de dados 'test-db4' para receber uma única linha pela chave primária '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 uma única linha do banco de dados é bastante simples: abra o banco de dados e especifique o repositório de objetos e o valor da chave primária da linha de onde você quer receber os dados. Como o método get() retorna uma promessa, é possível usar await nela.

Atualizar dados

Para atualizar dados, chame o método put() no repositório de objetos. O método put() é semelhante ao método add() e também pode ser usado no lugar de add() para criar dados. Veja um exemplo básico de como usar put() para atualizar uma linha em um repositório de objetos pelo valor da chave primária:

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();

Como outros métodos, esse retorna uma promessa. Também é possível usar put() como parte de uma transação. Veja um exemplo que usa o armazenamento 'foods' anterior que atualiza o preço do sanduíche e dos ovos:

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();

A forma como os itens são atualizados depende de como você define uma chave. Se você definir um keyPath, cada linha no repositório de objetos será associada a uma chave inline. O exemplo anterior atualiza as linhas com base nessa chave. Quando você atualiza as linhas nessa situação, é necessário especificar essa chave para atualizar o item apropriado no repositório de objetos. Também é possível criar uma chave fora de linha definindo um autoIncrement como a chave primária.

Excluir dados

Para excluir dados, chame o método delete() no repositório 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();

Assim como add() e put(), é possível usar isso como parte de uma transação:

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();

A estrutura da interação com o banco de dados é igual à das outras operações. Verifique se toda a transação foi concluída incluindo o método tx.done na matriz que você transmite para Promise.all.

Como receber todos os dados

Até agora, você só recuperou objetos da loja um por um. Também é possível recuperar todos os dados, ou um subconjunto, de um repositório de objetos ou índice usando o método getAll() ou cursores.

O método getAll()

A maneira mais simples de recuperar todos os dados de um repositório de objetos é chamar getAll() no repositório ou índice de objetos, desta forma:

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();

Esse método retorna todos os objetos no repositório de objetos, sem nenhuma restrição. É a maneira mais direta de receber todos os valores de um repositório de objetos, mas também a menos flexível.

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();

Este exemplo chama getAll() no repositório de objetos 'foods'. Isso retorna todos os objetos de 'foods', ordenados pela chave primária.

Como usar cursores

Os cursores são uma maneira mais flexível de recuperar vários objetos. Um cursor seleciona cada objeto em um repositório de objetos ou índice, um por um, permitindo que você faça algo com os dados quando eles são selecionados. Os cursores, assim como as outras operações de banco de dados, funcionam em transações.

Para criar um cursor, chame openCursor() no repositório de objetos como parte de uma transação. Usando o repositório 'foods' dos exemplos anteriores, veja como avançar um cursor por todas as linhas de dados em um repositório 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();

Nesse caso, a transação é aberta no modo 'readonly', e o método openCursor dela é chamado. Em um loop while subsequente, a linha na posição atual do cursor pode ter as propriedades key e value lidas, e você pode operar esses valores da maneira que fizer mais sentido para seu app. Quando estiver tudo pronto, chame o método continue() do objeto cursor para ir para a próxima linha. O loop while é encerrado quando o cursor chega ao fim do conjunto de dados.

Usar cursores com intervalos e índices

Os índices permitem buscar os dados em um repositório de objetos por uma propriedade diferente da chave primária. É possível criar um índice em qualquer propriedade, que se torna o keyPath do índice, especificar um intervalo nessa propriedade e receber os dados dentro do intervalo usando getAll() ou um cursor.

Defina o intervalo usando o objeto IDBKeyRange e qualquer um dos seguintes métodos:

Os métodos upperBound() e lowerBound() especificam os limites superior e inferior do intervalo.

IDBKeyRange.lowerBound(indexKey);

ou:

IDBKeyRange.upperBound(indexKey);

Cada um deles usa um argumento: o valor keyPath do índice para o item que você quer especificar como o limite superior ou inferior.

O método bound() especifica um limite superior e um inferior:

IDBKeyRange.bound(lowerIndexKey, upperIndexKey);

O intervalo dessas funções é inclusivo por padrão, o que significa que ele inclui os dados especificados como os limites do intervalo. Para deixar esses valores de fora, especifique o intervalo como exclusivo transmitindo true como o segundo argumento para lowerBound() ou upperBound(), ou como o terceiro e o quarto argumentos de bound(), para os limites inferior e superior, respectivamente.

O próximo exemplo usa um índice na propriedade 'price' no repositório de objetos 'foods'. A loja agora também tem um formulário anexado com duas entradas para os limites superior e inferior do intervalo. Use o código abaixo para encontrar alimentos com preços entre esses limites:

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);

O código de exemplo primeiro recebe os valores dos limites e verifica se eles existem. O próximo bloco de código decide qual método usar para limitar o intervalo com base nos valores. Na interação com o banco de dados, abra o repositório de objetos na transação como de costume e, em seguida, abra o índice 'price' no repositório de objetos. O índice 'price' permite pesquisar itens por preço.

Em seguida, o código abre um cursor no índice e transmite o intervalo. O cursor retorna uma promessa que representa o primeiro objeto no intervalo ou undefined se não houver dados no intervalo. O método cursor.continue() retorna um cursor que representa o próximo objeto e continua pelo loop até que você chegue ao fim do intervalo.

Controle de versões de banco de dados

Ao chamar o método openDB(), é possível especificar o número da versão do banco de dados no segundo parâmetro. Em todos os exemplos deste guia, a versão foi definida como 1, mas um banco de dados pode ser atualizado para uma nova versão se você precisar modificá-lo de alguma forma. Se a versão especificada for maior que a do banco de dados atual, o callback upgrade no objeto de evento será executado, permitindo que você adicione novos repositórios de objetos e índices ao banco de dados.

O objeto db no callback upgrade tem uma propriedade oldVersion especial, que indica o número da versão do banco de dados que o navegador pode acessar. É possível transmitir esse número de versão para uma instrução switch para executar blocos de código dentro do callback upgrade com base no número da versão do banco de dados existente. Veja um exemplo:

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');
    }
  }
});

Este exemplo define a versão mais recente do banco de dados como 2. Quando esse código é executado pela primeira vez, o banco de dados ainda não existe no navegador. Portanto, oldVersion é 0, e a instrução switch começa em case 0. No exemplo, isso adiciona um repositório de objetos 'store' ao banco de dados.

Ponto-chave: em instruções switch, geralmente há um break após cada bloco case, mas isso não é usado aqui de forma deliberada. Dessa forma, se o banco de dados atual estiver algumas versões desatualizado ou se não existir, o código vai continuar pelos demais blocos case até que esteja atualizado. No exemplo, o navegador continua a execução por meio de case 1, criando um índice name no repositório de objetos store.

Para criar um índice 'description' no repositório de objetos 'store', atualize o número da versão e adicione um novo bloco case da seguinte maneira:

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');
    }
  }
});

Se o banco de dados criado no exemplo anterior ainda existir no navegador, quando isso for executado, oldVersion será 2. O navegador ignora case 0 e case 1 e executa o código em case 2, que cria um índice description. Depois disso, o navegador tem um banco de dados na versão 3 que contém um repositório de objetos store com os índices name e description.

Leitura adicional

Os recursos a seguir fornecem mais informações e contexto para o uso do IndexedDB.

Documentação do IndexedDB

Limites de armazenamento de dados