Codelab da Web do Cloud Firestore

1. Visão geral

Gols

Neste codelab, você criará um app da Web de recomendação de restaurantes com tecnologia do Cloud Firestore.

img5.png

O que você aprenderá

  • Ler e gravar dados de um app da Web no Cloud Firestore
  • Detectar alterações nos dados do Cloud Firestore em tempo real
  • Usar o Firebase Authentication e as regras de segurança para proteger os dados do Cloud Firestore
  • Criar consultas complexas do Cloud Firestore

O que é necessário

Antes de iniciar este codelab, verifique se você instalou:

  • npm, que normalmente vem com Node.js: o Node 16 ou mais recente é recomendado.
  • O ambiente de desenvolvimento integrado/editor de texto de sua escolha, como WebStorm, VS Code ou Sublime.

2. Criar e configurar um projeto do Firebase

criar um projeto do Firebase

  1. No Console do Firebase, clique em Adicionar projeto e nomeie o projeto como FriendlyEats.

Lembre-se do ID do seu projeto do Firebase.

  1. Clique em Criar projeto.

O aplicativo que criaremos usa alguns serviços do Firebase disponíveis na Web:

  • Firebase Authentication: usado para identificar os usuários com facilidade
  • Cloud Firestore: usado para salvar dados estruturados no Cloud e receber notificações instantâneas quando os dados são atualizados.
  • Firebase Hosting: usado para hospedar e exibir seus recursos estáticos.

Para este codelab específico, já configuramos o Firebase Hosting. No entanto, para o Firebase Auth e o Cloud Firestore, vamos orientar você na configuração e ativação dos serviços usando o console do Firebase.

Ativar a autenticação anônima

Embora a autenticação não seja o foco deste codelab, é importante ter alguma forma de autenticação no nosso app. Usaremos o Login anônimo, o que significa que o usuário será conectado silenciosamente sem ser solicitado.

Ative a opção Login anônimo.

  1. No Console do Firebase, localize a seção Criar no painel de navegação à esquerda.
  2. Clique em Autenticação e na guia Método de login (ou clique aqui para acessá-la).
  3. Ative o provedor de login Anônimo e clique em Salvar.

img7.png

Isso permitirá que o app faça o login silencioso dos usuários quando eles acessarem o app da Web. Leia a documentação sobre autenticação anônima para saber mais.

Ativar o Cloud Firestore

O app usa o Cloud Firestore para salvar e receber informações e notas de restaurantes.

Será necessário ativar o Cloud Firestore. Na seção Build do Console do Firebase, clique em Firestore Database. Clique em Criar banco de dados no painel do Cloud Firestore.

O acesso aos dados no Cloud Firestore é controlado por regras de segurança. Falaremos mais sobre regras posteriormente neste codelab, mas antes precisamos definir algumas regras básicas nos nossos dados para começar. Na guia "Regras" do Console do Firebase, adicione as regras a seguir e clique em Publicar.

service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      //
      // WARNING: These rules are insecure! We will replace them with
      // more secure rules later in the codelab
      //
      allow read, write: if request.auth != null;
    }
  }
}

As regras acima restringem o acesso a dados aos usuários que fizeram login, o que impede que usuários não autenticados leiam ou gravem algo. Isso é melhor do que permitir o acesso público, mas ainda está longe de ser seguro. Vamos melhorar essas regras mais adiante no codelab.

3. Acessar o exemplo de código

Clone o repositório do GitHub (link em inglês) da linha de comando:

git clone https://github.com/firebase/friendlyeats-web

O exemplo de código precisa ter sido clonado no diretório 📁friendlyeats-web. De agora em diante, execute todos os comandos deste diretório:

cd friendlyeats-web/vanilla-js

Importar o app inicial

Abra ou importe o diretório 📁friendlyeats-web usando seu ambiente de desenvolvimento integrado (WebStorm, Atom, Sublime, Visual Studio Code etc.). Esse diretório contém o código inicial do codelab, que é um app de recomendação de restaurantes ainda não funcional. Ele será usado ao longo deste codelab, então você precisará editar o código nesse diretório em breve.

4. Instalar a interface de linha de comando do Firebase

A interface de linha de comando (CLI) do Firebase permite disponibilizar seu app da Web localmente e implantá-lo no Firebase Hosting.

  1. Instale a CLI executando o seguinte comando npm:
npm -g install firebase-tools
  1. Verifique se a CLI foi instalada corretamente executando o seguinte comando:
firebase --version

Verifique se a versão da CLI do Firebase é a v7.4.0 ou mais recente.

  1. Autorize a CLI do Firebase executando o seguinte comando:
firebase login

Definimos o modelo de app da Web para extrair a configuração do seu app para o Firebase Hosting do diretório local e dos arquivos dele. Mas, para isso, precisamos associar o app ao projeto do Firebase.

  1. Confira se a linha de comando está acessando o diretório local do app.
  2. Associe o app ao projeto do Firebase executando o seguinte comando:
firebase use --add
  1. Quando solicitado, selecione o ID do projeto e atribua um alias a ele.

Um alias é útil quando você tem vários ambientes (produção, preparo etc). No entanto, para este codelab, vamos usar apenas o alias de default.

  1. Siga as instruções restantes na linha de comando.

5. Executar o servidor local

Estamos prontos para começar a trabalhar no nosso app. Vamos executar nosso app localmente.

  1. Execute o seguinte comando da CLI do Firebase:
firebase emulators:start --only hosting
  1. A linha de comando vai exibir a seguinte resposta:
hosting: Local server: http://localhost:5000

Estamos usando o emulador do Firebase Hosting para disponibilizar nosso app localmente. Agora o app da Web estará disponível em http://localhost:5000.

  1. Abra o aplicativo em http://localhost:5000.

Você verá uma cópia do FriendlyEats que foi conectada ao seu projeto do Firebase.

O app se conectou automaticamente ao projeto do Firebase e fez login silenciosamente como um usuário anônimo.

img2.png

6. Gravar dados no Cloud Firestore

Nesta seção, vamos gravar alguns dados no Cloud Firestore para preencher a interface do app. Isso pode ser feito manualmente no Console do Firebase, mas faremos isso no próprio app para demonstrar uma gravação básica do Cloud Firestore.

Modelo de dados

Os dados do Firestore são divididos em coleções, documentos, campos e subcoleções. Armazenaremos cada restaurante como um documento em uma coleção de nível superior chamada restaurants.

img3.png

Mais tarde, armazenaremos cada avaliação em uma subcoleção chamada ratings em cada restaurante.

img4.png

Adicionar restaurantes ao Firestore

O principal objeto do modelo no nosso app é um restaurante. Vamos escrever um código que adicione um documento de restaurante à coleção restaurants.

  1. Nos arquivos transferidos por download, abra scripts/FriendlyEats.Data.js.
  2. Encontre a função FriendlyEats.prototype.addRestaurant.
  3. Substitua a função inteira pelo código a seguir.

FriendlyEats.Data.js (link em inglês)

FriendlyEats.prototype.addRestaurant = function(data) {
  var collection = firebase.firestore().collection('restaurants');
  return collection.add(data);
};

O código acima adiciona um novo documento à coleção restaurants. Os dados do documento são provenientes de um objeto JavaScript simples. Para isso, primeiro recuperamos uma referência a uma coleção do Cloud Firestore restaurants e, em seguida, addativamos os dados.

Vamos adicionar restaurantes.

  1. Volte para o app FriendlyEats no navegador e atualize.
  2. Clique em Add Mock Data.

O app vai gerar automaticamente um conjunto aleatório de objetos de restaurante e chamar a função addRestaurant. No entanto, você ainda não vai conferir os dados no seu app da Web real porque ainda precisamos implementar a recuperação dos dados (a próxima seção do codelab).

No entanto, se você navegar até a guia Cloud Firestore no Console do Firebase, verá novos documentos na coleção restaurants.

img6.png

Parabéns, você acabou de gravar dados de um app da Web no Cloud Firestore.

Na próxima seção, você vai aprender a recuperar dados do Cloud Firestore e mostrá-los no seu app.

7. Mostrar dados do Cloud Firestore

Nesta seção, você vai aprender a recuperar dados do Cloud Firestore e mostrá-los no seu app. As duas etapas principais são criar uma consulta e adicionar um listener de snapshot. Esse listener é notificado sobre todos os dados existentes que correspondem à consulta e recebe atualizações em tempo real.

Primeiro, vamos construir a consulta que vai exibir a lista padrão não filtrada de restaurantes.

  1. Volte para o arquivo scripts/FriendlyEats.Data.js.
  2. Encontre a função FriendlyEats.prototype.getAllRestaurants.
  3. Substitua a função inteira pelo código a seguir.

FriendlyEats.Data.js (link em inglês)

FriendlyEats.prototype.getAllRestaurants = function(renderer) {
  var query = firebase.firestore()
      .collection('restaurants')
      .orderBy('avgRating', 'desc')
      .limit(50);

  this.getDocumentsInQuery(query, renderer);
};

No código acima, construímos uma consulta que recuperará até 50 restaurantes da coleção de nível superior chamada restaurants, que são ordenados pela avaliação média (atualmente, todos zeros). Depois de declarar essa consulta, a transmitimos para o método getDocumentsInQuery(), que é responsável por carregar e renderizar os dados.

Para isso, vamos adicionar um listener de snapshot.

  1. Volte para o arquivo scripts/FriendlyEats.Data.js.
  2. Encontre a função FriendlyEats.prototype.getDocumentsInQuery.
  3. Substitua a função inteira pelo código a seguir.

FriendlyEats.Data.js (link em inglês)

FriendlyEats.prototype.getDocumentsInQuery = function(query, renderer) {
  query.onSnapshot(function(snapshot) {
    if (!snapshot.size) return renderer.empty(); // Display "There are no restaurants".

    snapshot.docChanges().forEach(function(change) {
      if (change.type === 'removed') {
        renderer.remove(change.doc);
      } else {
        renderer.display(change.doc);
      }
    });
  });
};

No código acima, query.onSnapshot acionará o callback sempre que houver uma mudança no resultado da consulta.

  • Na primeira vez, o callback é acionado com todo o conjunto de resultados da consulta, ou seja, toda a coleção restaurants do Cloud Firestore. Em seguida, ela transmite todos os documentos individuais para a função renderer.display.
  • Quando um documento é excluído, change.type é igual a removed. Nesse caso, vamos chamar uma função que remove o restaurante da interface.

Agora que implementamos os dois métodos, atualize o app e verifique se os restaurantes que vimos anteriormente no Console do Firebase estão visíveis nele. Se você concluiu esta seção, seu app está lendo e gravando dados com o Cloud Firestore.

Conforme sua lista de restaurantes muda, esse listener é atualizado automaticamente. Tente acessar o console do Firebase e excluir manualmente um restaurante ou mudar o nome dele. As mudanças vão aparecer no seu site imediatamente.

img5.png

8. Dados Get()

Até agora, mostramos como usar onSnapshot para recuperar atualizações em tempo real. No entanto, nem sempre é isso que queremos. Às vezes, faz mais sentido buscar os dados apenas uma vez.

Queremos implementar um método que é acionado quando um usuário clica em um restaurante específico no seu app.

  1. Volte para o arquivo scripts/FriendlyEats.Data.js.
  2. Encontre a função FriendlyEats.prototype.getRestaurant.
  3. Substitua a função inteira pelo código a seguir.

FriendlyEats.Data.js (link em inglês)

FriendlyEats.prototype.getRestaurant = function(id) {
  return firebase.firestore().collection('restaurants').doc(id).get();
};

Depois de implementar esse método, você poderá visualizar as páginas de cada restaurante. Basta clicar em um restaurante na lista para ver a página de detalhes do restaurante:

img1.png

Por enquanto, não é possível adicionar classificações porque ainda precisamos implementar essa opção mais adiante no codelab.

9. Classificar e filtrar dados

No momento, nosso app exibe uma lista de restaurantes, mas não há como o usuário filtrar com base nas necessidades dele. Nesta seção, você vai usar a consulta avançada do Cloud Firestore para ativar a filtragem.

Este é um exemplo de consulta simples para buscar todos os restaurantes Dim Sum:

var filteredQuery = query.where('category', '==', 'Dim Sum')

Como o nome dele indica, o método where() fará com que nossa consulta faça o download apenas dos membros da coleção cujos campos atendam às restrições definidas. Nesse caso, o download apenas de restaurantes em que category for Dim Sum será feito.

No nosso app, o usuário pode encadear vários filtros para criar consultas específicas, como "Pizza em São Paulo" ou "Frutos do mar em São Paulo solicitados por popularidade".

Vamos criar um método que cria uma consulta que vai filtrar nossos restaurantes com base em vários critérios selecionados pelos nossos usuários.

  1. Volte para o arquivo scripts/FriendlyEats.Data.js.
  2. Encontre a função FriendlyEats.prototype.getFilteredRestaurants.
  3. Substitua a função inteira pelo código a seguir.

FriendlyEats.Data.js (link em inglês)

FriendlyEats.prototype.getFilteredRestaurants = function(filters, renderer) {
  var query = firebase.firestore().collection('restaurants');

  if (filters.category !== 'Any') {
    query = query.where('category', '==', filters.category);
  }

  if (filters.city !== 'Any') {
    query = query.where('city', '==', filters.city);
  }

  if (filters.price !== 'Any') {
    query = query.where('price', '==', filters.price.length);
  }

  if (filters.sort === 'Rating') {
    query = query.orderBy('avgRating', 'desc');
  } else if (filters.sort === 'Reviews') {
    query = query.orderBy('numRatings', 'desc');
  }

  this.getDocumentsInQuery(query, renderer);
};

O código acima adiciona vários filtros where e uma única cláusula orderBy para criar uma consulta composta com base na entrada do usuário. Nossa consulta agora retornará apenas restaurantes que correspondam aos requisitos do usuário.

Atualize o app FriendlyEats no navegador e verifique se é possível filtrar por preço, cidade e categoria. Durante os testes, você verá erros no Console JavaScript do navegador semelhantes a este:

The query requires an index. You can create it here: https://console.firebase.google.com/project/project-id/database/firestore/indexes?create_composite=...

Esses erros ocorrem porque o Cloud Firestore exige índices para a maioria das consultas compostas. A exigência de índices em consultas mantém o Cloud Firestore rápido em escala.

Abrir o link da mensagem de erro abrirá automaticamente a interface de criação do índice no console do Firebase com os parâmetros corretos preenchidos. Na próxima seção, vamos gravar e implantar os índices necessários para esse aplicativo.

10. Implantar índices

Se não quisermos explorar cada caminho do nosso app e seguir cada um dos links de criação de índice, podemos implantar facilmente muitos índices de uma só vez usando a CLI do Firebase.

  1. No diretório local do app transferido por download, você encontrará um arquivo firestore.indexes.json.

Esse arquivo descreve todos os índices necessários para todas as combinações possíveis de filtros.

firestore.indexes.json (link em inglês)

{
 "indexes": [
   {
     "collectionGroup": "restaurants",
     "queryScope": "COLLECTION",
     "fields": [
       { "fieldPath": "city", "order": "ASCENDING" },
       { "fieldPath": "avgRating", "order": "DESCENDING" }
     ]
   },

   ...

 ]
}
  1. Implante os índices com o seguinte comando:
firebase deploy --only firestore:indexes

Após alguns minutos, os índices estarão ativos e as mensagens de erro desaparecerão.

11. Gravar dados em uma transação

Nesta seção, vamos adicionar um recurso para que os usuários enviem avaliações aos restaurantes. Até agora, todas as nossas gravações foram atômicas e relativamente simples. Se algum deles apresentar erro, provavelmente pediremos que o usuário tente novamente ou nosso app repetirá a gravação automaticamente.

Nosso aplicativo terá muitos usuários que desejam adicionar uma classificação para um restaurante, portanto, precisaremos coordenar várias leituras e gravações. Primeiro, a avaliação em si precisa ser enviada e, em seguida, a classificação count e average rating do restaurante precisam ser atualizadas. Se um deles falhar, mas não o outro, ficamos em um estado inconsistente em que os dados de uma parte do nosso banco de dados não correspondem aos dados de outra.

Felizmente, o Cloud Firestore oferece uma funcionalidade de transação que nos permite realizar várias leituras e gravações em uma única operação atômica, garantindo que nossos dados permaneçam consistentes.

  1. Volte para o arquivo scripts/FriendlyEats.Data.js.
  2. Encontre a função FriendlyEats.prototype.addRating.
  3. Substitua a função inteira pelo código a seguir.

FriendlyEats.Data.js (link em inglês)

FriendlyEats.prototype.addRating = function(restaurantID, rating) {
  var collection = firebase.firestore().collection('restaurants');
  var document = collection.doc(restaurantID);
  var newRatingDocument = document.collection('ratings').doc();

  return firebase.firestore().runTransaction(function(transaction) {
    return transaction.get(document).then(function(doc) {
      var data = doc.data();

      var newAverage =
          (data.numRatings * data.avgRating + rating.rating) /
          (data.numRatings + 1);

      transaction.update(document, {
        numRatings: data.numRatings + 1,
        avgRating: newAverage
      });
      return transaction.set(newRatingDocument, rating);
    });
  });
};

No bloco acima, acionamos uma transação para atualizar os valores numéricos de avgRating e numRatings no documento do restaurante. Ao mesmo tempo, adicionamos o novo rating à subcoleção ratings.

12. Proteger seus dados

No início deste codelab, definimos as regras de segurança do nosso app para abrir completamente o banco de dados para qualquer leitura ou gravação. Em um aplicativo real, gostaríamos de definir regras muito mais detalhadas para evitar acesso ou modificação indesejável aos dados.

  1. Na seção Build do Console do Firebase, clique em Firestore Database.
  2. Clique na guia Regras na seção do Cloud Firestore ou clique aqui.
  3. Substitua os padrões pelas regras a seguir e clique em Publicar.

firestore.rules (em inglês)

rules_version = '2';
service cloud.firestore {

  // Determine if the value of the field "key" is the same
  // before and after the request.
  function unchanged(key) {
    return (key in resource.data) 
      && (key in request.resource.data) 
      && (resource.data[key] == request.resource.data[key]);
  }

  match /databases/{database}/documents {
    // Restaurants:
    //   - Authenticated user can read
    //   - Authenticated user can create/update (for demo purposes only)
    //   - Updates are allowed if no fields are added and name is unchanged
    //   - Deletes are not allowed (default)
    match /restaurants/{restaurantId} {
      allow read: if request.auth != null;
      allow create: if request.auth != null;
      allow update: if request.auth != null
                    && (request.resource.data.keys() == resource.data.keys()) 
                    && unchanged("name");
      
      // Ratings:
      //   - Authenticated user can read
      //   - Authenticated user can create if userId matches
      //   - Deletes and updates are not allowed (default)
      match /ratings/{ratingId} {
        allow read: if request.auth != null;
        allow create: if request.auth != null
                      && request.resource.data.userId == request.auth.uid;
      }
    }
  }
}

Essas regras restringem o acesso para garantir que os clientes só façam mudanças seguras. Exemplo:

  • Atualizações em um documento de restaurante podem alterar apenas as notas, não o nome ou quaisquer outros dados imutáveis.
  • As classificações só poderão ser criadas se o ID do usuário corresponder ao usuário conectado, o que impede o spoofing.

Como alternativa ao uso do Console do Firebase, é possível usar a CLI do Firebase para implantar regras no seu projeto. O arquivo firestore.rules no seu diretório de trabalho já contém as regras acima. Para implantar essas regras a partir do seu sistema de arquivos local (em vez de usar o Console do Firebase), execute o seguinte comando:

firebase deploy --only firestore:rules

13. Conclusão

Neste codelab, você aprendeu a realizar leituras e gravações básicas e avançadas com o Cloud Firestore e como proteger o acesso a dados com regras de segurança. Encontre a solução completa no repositório quickstarts-js.

Para saber mais sobre o Cloud Firestore, acesse os seguintes recursos:

14. [Opcional] Aplicar com o App Check

O App Check do Firebase oferece proteção, ajudando a validar e impedir o tráfego indesejado no seu app. Nesta etapa, você protegerá o acesso aos seus serviços adicionando o App Check com o reCAPTCHA Enterprise.

Primeiro, ative o App Check e o reCAPTCHA.

Como ativar o reCAPTCHA Enterprise

  1. No console do Cloud, encontre e selecione reCaptcha Enterprise em "Segurança".
  2. Ative o serviço conforme solicitado e clique em Criar chave.
  3. Insira um nome de exibição conforme solicitado e selecione Site como o tipo de plataforma.
  4. Adicione os URLs implantados à lista de domínios e verifique se a opção "Usar desafio da caixa de seleção" está desmarcada.
  5. Clique em Criar chave e armazene a chave gerada em algum lugar para protegê-la. Você vai precisar dele mais adiante nesta etapa.

Como ativar o App Check

  1. No Console do Firebase, localize a seção Build no painel esquerdo.
  2. Clique em App Check e depois no botão Get Started (ou redirecione diretamente para o console).
  3. Clique em Registrar, digite sua chave reCAPTCHA Enterprise quando solicitado e clique em Save.
  4. Na visualização de APIs, selecione Armazenamento e clique em Aplicar. Faça o mesmo para o Cloud Firestore.

O App Check foi aplicado. Atualize o app e tente criar/conferir um restaurante. Você vai receber a seguinte mensagem de erro:

Uncaught Error in snapshot listener: FirebaseError: [code=permission-denied]: Missing or insufficient permissions.

Isso significa que o App Check está bloqueando solicitações não validadas por padrão. Agora, vamos adicionar a validação ao app.

Acesse o arquivo FriendlyEats.View.js, atualize a função initAppCheck e adicione sua chave reCAPTCHA para inicializar o App Check.

FriendlyEats.prototype.initAppCheck = function() {
    var appCheck = firebase.appCheck();
    appCheck.activate(
    new firebase.appCheck.ReCaptchaEnterpriseProvider(
      /* reCAPTCHA Enterprise site key */
    ),
    true // Set to true to allow auto-refresh.
  );
};

A instância appCheck é inicializada com um ReCaptchaEnterpriseProvider com sua chave, e isTokenAutoRefreshEnabled permite que os tokens sejam atualizados automaticamente no app.

Para ativar os testes locais, encontre a seção em que o app foi inicializado no arquivo FriendlyEats.js e adicione a seguinte linha à função FriendlyEats.prototype.initAppCheck:

if(isLocalhost) {
  self.FIREBASE_APPCHECK_DEBUG_TOKEN = true;
}

Isso registrará um token de depuração no console do seu aplicativo da Web local, semelhante a:

App Check debug token: 8DBDF614-649D-4D22-B0A3-6D489412838B. You will need to add it to your app's App Check settings in the Firebase console for it to work.

Agora acesse a visualização de apps do App Check no Console do Firebase.

Clique no menu flutuante e selecione Gerenciar tokens de depuração.

Em seguida, clique em Adicionar token de depuração e cole o token de depuração do seu console, conforme solicitado.

Parabéns! O App Check agora deve estar funcionando no seu app.