Trabalhar com IndexedDB

Neste guia, abordamos 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 usadas como await para uma sintaxe mais concisa. Isso simplifica a API e mantém a estrutura dela.

O que é o IndexedDB?

O IndexedDB é um sistema de armazenamento NoSQL em grande escala que permite armazenar praticamente qualquer item no navegador do usuário. Além das ações comuns de pesquisa, busca e put, o IndexedDB também é compatível com transações e é adequado para armazenar grandes quantidades de dados estruturados.

Cada banco de dados IndexedDB é exclusivo para uma origin (normalmente, o domínio ou subdomínio do site), o que significa que ele não pode acessar ou ser acessado por qualquer outra origem. Os limites de armazenamento de dados geralmente são grandes, se existirem, mas diferentes navegadores processam limites e remoções 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 de IndexedDB. Ela contém os armazenamentos de objetos, que, por sua vez, contêm os dados que você quer manter. É possível criar vários bancos de dados com quaisquer nomes que você escolher.
Repositório de objetos
Um bucket individual para armazenar dados, semelhante às tabelas em bancos de dados relacionais. Normalmente, há um armazenamento de objetos para cada tipo (não JavaScript) de dados que você armazena. Ao contrário das tabelas de banco de dados, os tipos de dados JavaScript em um armazenamento não precisam ser consistentes. Por exemplo, se um app tiver um repositório de objetos people que contém informações sobre três pessoas, as propriedades de idade delas poderão ser 53, 'twenty-five' e unknown.
Índice
Um tipo de armazenamento de objetos para organizar dados em outro armazenamento de objetos (chamado de armazenamento de objetos de referência) por uma propriedade individual dos dados. O índice é usado por essa propriedade para recuperar registros no armazenamento de objetos. Por exemplo, se você estiver armazenando pessoas, poderá buscar mais tarde pelo nome, idade ou animais 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-gravação sem o risco de conflitos com outras linhas de execução atuando no banco de dados ao mesmo tempo.
Cursor
Um mecanismo para iterar vários registros em um banco de dados.

Como verificar a compatibilidade com IndexedDB

O IndexedDB tem suporte universal (link em inglês). No entanto, se você estiver trabalhando com navegadores mais antigos, não é uma má 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, você pode criar vários bancos de dados com o nome que quiser. 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, número de versão e um objeto de eventos para configurar o banco de dados.

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

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

Como trabalhar com armazenamentos de objetos

Um banco de dados IndexedDB contém um ou mais armazenamentos 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 armazenamento de objetos para cada tipo de dados que precisam ser mantidos. Por exemplo, um site que mantém perfis e notas de usuário pode ter um repositório de objetos people, que contém objetos person, e um repositório de objeto notes, contendo objetos note.

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

Confira a seguir um exemplo de como usar o 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 armazenamento de objetos e, como antes, o trabalho de criação do armazenamento de objetos é feito no método upgrade() do objeto de evento. No entanto, como o navegador gera um erro se você tentar criar um armazenamento de objetos que já existe, recomendamos unir o método createObjectStore() em uma instrução if que verifica se esse armazenamento existe. No bloco if, chame createObjectStore() para criar um armazenamento de objetos chamado 'firstOS'.

Como definir chaves primárias

Ao definir armazenamentos de objetos, é possível definir como os dados são identificados exclusivamente nele usando uma chave primária. Você pode definir uma chave primária ao definir um caminho de chave ou usar 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 armazenamento de objeto people, é possível 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 armazenamento de objetos chamado 'people' e atribui a propriedade email como chave primária na opção keyPath.

Também é possível usar um gerador de chaves, como o autoIncrement. O gerador de chaves cria um valor exclusivo para cada objeto adicionado ao armazenamento. Por padrão, se você não especificar uma chave, o IndexedDB criará uma chave e a armazenará separadamente dos dados.

O exemplo a seguir cria um armazenamento de objetos chamado 'notes' e define a chave primária a 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 têm uma propriedade que é sempre exclusiva, é possível torná-los keyPath para impor essa exclusividade. Caso contrário, use um valor de incremento automático.

O código a seguir cria três armazenamentos de objetos, demonstrando as várias maneiras de definir chaves primárias em armazenamentos 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();

Como definir índices

Índices são um tipo de armazenamento de objetos usado para recuperar dados do armazenamento 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 caminho de chave em vez da chave primária do armazenamento de referência. Os índices precisam ser criados quando você cria seus armazenamentos 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 armazenamento 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 funciona: unique e multiEntry. Se unique for definido como true, o índice não permitirá valores duplicados para uma única chave. Em seguida, multiEntry determina como createIndex() se comporta quando a propriedade indexada é uma matriz. Se estiver definido como true, createIndex() vai adicionar uma entrada no índice para cada elemento da matriz. Caso contrário, ele 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 armazenamentos de objetos 'people' e 'notes' têm índices. Para criar os índices, primeiro atribua o resultado de createObjectStore() (um objeto de armazenamento de objetos) a uma variável para que você possa chamar createIndex() nele.

Como trabalhar com dados

Esta seção descreve como criar, ler, atualizar e excluir dados. Essas operações são todas 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, você pode chamar .then() no objeto do 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. Recebe o objeto do banco de dados.
  2. Abrir transação no banco de dados.
  3. Abrir armazenamento de objetos na transação.
  4. Executar operação no armazenamento de objetos.

Uma transação pode ser considerada um wrapper seguro em torno de uma operação ou de um 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 a um ou mais armazenamentos de objetos, que você define quando abre a transação. Eles podem ser somente leitura ou de leitura e gravação. Isso significa 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 armazenamento de objetos a que você quer adicionar os dados, e o segundo argumento é um objeto que contém os campos e os dados associados que você quer adicionar. Este é 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 dentro de uma transação. Portanto, mesmo que a promessa seja resolvida com sucesso, isso não significa necessariamente que a operação funcionou. Para garantir que a operação de adição tenha sido realizada, é necessário verificar 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 em caso de erros. Você precisa realizar essa verificação para todas as operações de "gravação", porque essa é sua única maneira de saber que as alterações no banco de dados realmente ocorreram.

O código a seguir mostra o uso do método add() em 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 armazenamento de objetos, se necessário), você precisará abrir uma transação chamando o método transaction() nela. Esse método usa um argumento para a loja em que você quer fazer transações, bem como o modo. Neste caso, estamos interessados em gravar na loja, 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 delas retorna uma promessa:

  1. Adicionando um registro de um sanduíche saboroso.
  2. Adicionando um registro para alguns ovos.
  3. Indicaçã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 boa e ergonômica de fazer isso. Promise.all aceita uma matriz de promessas e termina quando todas as promessas passadas a ele 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 ele. É possível usar await na chamada Promise.all para que ela seja concluída quando a transação for concluída.

Ler dados

Para ler dados, chame o método get() na instância do banco de dados recuperada usando o método openDB(). get() usa o nome do armazenamento e o valor da chave primária do objeto que você quer recuperar. Aqui está 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 acontece com add(), o método get() retorna uma promessa, para que você possa await, se preferir, ou usar o callback .then() da promessa.

O exemplo a seguir usa o método get() no armazenamento 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 é bem simples: abra o banco de dados e especifique o armazenamento de objetos e o valor da chave primária da linha da qual você quer receber dados. Como o método get() retorna uma promessa, você pode usar await.

Atualizar dados

Para atualizar dados, chame o método put() no armazenamento de objetos. O método put() é semelhante ao add() e também pode ser usado no lugar de add() para criar dados. Confira um exemplo básico de como usar put() para atualizar uma linha em um armazenamento 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 método retorna uma promessa. Também é possível usar put() como parte de uma transação. Veja um exemplo usando a loja '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 maneira como os itens são atualizados depende de como você define uma chave. Se você definir um keyPath, cada linha no armazenamento de objetos será associada a uma chave inline. O exemplo anterior atualiza linhas com base nessa chave e, quando você atualiza linhas nessa situação, precisa especificar essa chave para atualizar o item apropriado no armazenamento 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 armazenamento 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 do banco de dados é a mesma das outras operações. Lembre-se de verificar se toda a transação foi concluída incluindo o método tx.done na matriz transmitida para Promise.all.

Como obter todos os dados

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

Método getAll()

A maneira mais simples de recuperar todos os dados de um armazenamento de objetos é chamar getAll() no armazenamento 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 armazenamento de objetos, sem qualquer restrição. É a maneira mais direta de conseguir todos os valores de um armazenamento 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 armazenamento 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 armazenamento de objetos ou indexa um por um, permitindo que você faça algo com os dados quando eles estiverem selecionados. Cursores, assim como as outras operações de banco de dados, funcionam em transações.

Para criar um cursor, chame openCursor() no armazenamento de objetos como parte de uma transação. Veja como avançar um cursor por todas as linhas de dados em um armazenamento de objetos usando o armazenamento 'foods' dos exemplos anteriores:

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 é chamado. Em uma repetição while subsequente, a linha na posição atual do cursor pode ter suas propriedades key e value lidas, e você pode operar nesses valores da maneira que fizer mais sentido para seu aplicativo. Quando estiver pronto, chame o método continue() do objeto cursor para ir para a próxima linha, e o loop while será encerrado quando o cursor chegar ao fim do conjunto de dados.

Usar cursores com intervalos e índices

Os índices permitem buscar os dados em um armazenamento 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 acessar 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 máximos e mínimos do intervalo.

IDBKeyRange.lowerBound(indexKey);

ou:

IDBKeyRange.upperBound(indexKey);

Cada uma delas usa um argumento: o valor keyPath do índice para o item que você quer especificar como o limite máximo ou mínimo.

O método bound() especifica um limite máximo e mínimo:

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 de fora esses valores, especifique o intervalo como exclusivo transmitindo true como o segundo argumento para lowerBound() ou upperBound(), ou como o terceiro e quarto argumentos de bound(), para os limites inferior e superior, respectivamente.

O próximo exemplo usa um índice na propriedade 'price' no armazenamento de objetos 'foods'. O repositório agora também tem um formulário anexado com duas entradas para os limites máximo e mínimo do intervalo. Use o código a seguir 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);

Primeiro, o código de exemplo acessa 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 do banco de dados, abra o armazenamento de objetos na transação como de costume e, em seguida, abra o índice 'price' no armazenamento de objetos. O índice 'price' permite pesquisar itens por preço.

O código abre um cursor no índice e passa 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é chegar ao fim do intervalo.

Controle de versões do banco de dados

Ao chamar o método openDB(), especifique o número da versão do banco de dados no segundo parâmetro. Em todos os exemplos neste guia, a versão foi definida como 1, mas é possível fazer upgrade de um banco de dados para uma nova versão caso seja necessário modificar algo. Se a versão especificada for mais recente que a versão 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 a que o navegador tem acesso. É possível transmitir esse número de versão em 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. 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, então oldVersion é 0, e a instrução switch começa em case 0. No exemplo, isso adiciona um armazenamento de objetos 'store' ao banco de dados.

Importante: nas instruções switch, geralmente há um break após cada bloco case, mas isso não é usado aqui deliberadamente. Dessa forma, se o banco de dados já existente estiver algumas versões atrás de outras ou se ele não existir, o código continuará através do restante dos blocos case até ficar atualizado. Portanto, no exemplo, o navegador continua a execução com case 1, criando um índice name no armazenamento de objetos store.

Para criar um índice 'description' no armazenamento 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 armazenamento de objetos store com índices name e description.

Leia mais

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

Documentação do IndexedDB

Limites de armazenamento de dados