Codelab web de Cloud Firestore

1. Descripción general

Objetivos

En este codelab, compilarás una app web para recomendar restaurantes con la tecnología de Cloud Firestore.

img5.png

Qué aprenderás

  • Lee y escribe datos en Cloud Firestore desde una app web
  • Detecta cambios en los datos de Cloud Firestore en tiempo real
  • Usa Firebase Authentication y reglas de seguridad para proteger los datos de Cloud Firestore
  • Escribe consultas complejas en Cloud Firestore

Requisitos

Antes de comenzar este codelab, asegúrate de haber instalado lo siguiente:

  • npm que, por lo general, incluye Node.js (se recomienda Node.js 16 o una versión posterior)
  • El IDE o editor de texto que prefieras, como WebStorm, VS Code o Sublime

2. Crea y configura un proyecto de Firebase

Crea un proyecto de Firebase

  1. En Firebase console, haz clic en Agregar proyecto y asígnale el nombre FriendlyEats al proyecto.

Recuerda el ID del proyecto de Firebase.

  1. Haz clic en Crear proyecto.

La aplicación que compilaremos usa algunos servicios de Firebase disponibles en la Web:

  • Firebase Authentication para identificar a tus usuarios con facilidad
  • Cloud Firestore para guardar datos estructurados en la nube y recibir notificaciones instantáneas cuando se actualicen los datos
  • Firebase Hosting, para alojar y entregar tus elementos estáticos

Para este codelab específico, ya configuramos Firebase Hosting. Sin embargo, en el caso de Firebase Auth y Cloud Firestore, te guiaremos en la configuración y habilitación de los servicios usando Firebase console.

Habilitar la autenticación anónima

Si bien la autenticación no es el tema central de este codelab, es importante tener alguna forma de autenticación en nuestra app. Usaremos acceso anónimo, lo que significa que el usuario accederá de forma silenciosa sin que se le solicite.

Deberás habilitar el acceso anónimo.

  1. En Firebase console, busca la sección Compilación en el panel de navegación izquierdo.
  2. Haz clic en Authentication y, luego, en la pestaña Sign-in method (o haz clic aquí para ir directamente allí).
  3. Habilita el proveedor de acceso Anónimo y, luego, haz clic en Guardar.

img7.png

Esto permitirá que la aplicación acceda de manera silenciosa a tus usuarios cuando accedan a la app web. Lee la documentación de autenticación anónima para obtener más información.

Habilite Cloud Firestore

La app usa Cloud Firestore para guardar y recibir información y calificaciones de los restaurantes.

Deberás habilitar Cloud Firestore. En la sección Compilación de Firebase console, haz clic en Base de datos de Firestore. Haz clic en Crear base de datos en el panel de Cloud Firestore.

El acceso a los datos en Cloud Firestore se controla con reglas de seguridad. Hablaremos más sobre las reglas más adelante en este codelab, pero primero debemos establecer algunas reglas básicas en nuestros datos para comenzar. En la pestaña Reglas de Firebase console, agrega las siguientes reglas y, luego, haz clic en 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;
    }
  }
}

Las reglas anteriores restringen el acceso a los datos a los usuarios que acceden a su cuenta, lo que impide que los usuarios no autenticados lean o escriban. Esto es mejor que permitir el acceso público, pero aún está lejos de ser seguro. Mejoraremos estas reglas más adelante en el codelab.

3. Obtén el código de muestra

Clona el repositorio de GitHub desde la línea de comandos:

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

El código de muestra se debería haber clonado en el directorio Resultado friendlyeats-web. A partir de ahora, asegúrate de ejecutar todos los comandos desde este directorio:

cd friendlyeats-web/vanilla-js

Importa la app de partida

Usa tu IDE (WebStorm, Atom, Sublime, Visual Studio Code, etc.) para abrir o importar el directorio resultadosfriendlyeats-web. Este directorio contiene el código inicial para el codelab, que consiste en una app de recomendaciones de restaurantes que aún no funciona. Haremos que sea funcional a lo largo de este codelab, por lo que pronto deberás editar el código en ese directorio.

4. Instala la interfaz de línea de comandos de Firebase

La interfaz de línea de comandos (CLI) de Firebase te permite entregar tu aplicación web de forma local y, luego, implementarla en Firebase Hosting.

  1. Ejecuta el siguiente comando npm para instalar la CLI:
npm -g install firebase-tools
  1. Ejecuta el siguiente comando para verificar que la CLI se haya instalado de forma correcta:
firebase --version

Asegúrate de que la versión de Firebase CLI sea 7.4.0 o posterior.

  1. Ejecuta el siguiente comando para autorizar Firebase CLI:
firebase login

Configuramos la plantilla de la aplicación web para que extraiga la configuración de tu app para Firebase Hosting del directorio y los archivos locales de tu app. Sin embargo, para hacerlo, necesitamos asociar tu app con tu proyecto de Firebase.

  1. Asegúrate de que la línea de comandos acceda al directorio local de tu app.
  2. Ejecuta el siguiente comando para asociar la app con el proyecto de Firebase:
firebase use --add
  1. Cuando se te solicite, selecciona el ID de tu proyecto y asígnale un alias a tu proyecto de Firebase.

Un alias es útil si tienes varios entornos (producción, etapa de pruebas, etcétera). Sin embargo, para este codelab, solo usaremos el alias de default.

  1. Sigue las instrucciones restantes en tu línea de comandos.

5. Ejecuta el servidor local

Estamos listos para comenzar a trabajar en nuestra app. Ejecutemos la app localmente.

  1. Ejecuta el siguiente comando de Firebase CLI:
firebase emulators:start --only hosting
  1. Tu línea de comandos debería mostrar la siguiente respuesta:
hosting: Local server: http://localhost:5000

Usamos el emulador de Firebase Hosting para entregar la app de manera local. Ahora, la aplicación web debería estar disponible en http://localhost:5000.

  1. Abre tu aplicación en http://localhost:5000.

Deberías ver tu copia de FriendlyEats, que se conectó a tu proyecto de Firebase.

La app se conectó automáticamente a tu proyecto de Firebase y te accedió como usuario anónimo de forma silenciosa.

img2.png

6. Escribe datos en Cloud Firestore

En esta sección, escribiremos algunos datos en Cloud Firestore para poder propagar la IU de la app. Esto se puede hacer de forma manual a través de Firebase console, pero lo haremos en la misma app para demostrar una escritura básica de Cloud Firestore.

Modelo de datos

Los datos de Firestore se dividen en colecciones, documentos, campos y subcolecciones. Almacenaremos cada restaurante como un documento en una colección de nivel superior llamada restaurants.

img3.png

Más adelante, almacenaremos cada opinión en una subcolección llamada ratings debajo de cada restaurante.

img4.png

Agrega restaurantes a Firestore

El objeto principal del modelo en nuestra app es un restaurante. Escribamos código que agregue un documento de restaurante a la colección restaurants.

  1. En los archivos que descargaste, abre scripts/FriendlyEats.Data.js.
  2. Busca la función FriendlyEats.prototype.addRestaurant.
  3. Reemplaza toda la función por el siguiente código.

FriendlyEats.Data.js

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

El código anterior agrega un documento nuevo a la colección restaurants. Los datos del documento provienen de un objeto JavaScript simple. Para ello, primero obtenemos una referencia a una colección de Cloud Firestore restaurants y, luego, addusamos los datos.

¡Agreguemos restaurantes!

  1. Regresa a la app de FRIENDEats en tu navegador y actualízala.
  2. Haz clic en Add Mock Data.

La app generará automáticamente un conjunto aleatorio de objetos de restaurantes y, luego, llamará a tu función addRestaurant. Sin embargo, aún no verás los datos en tu app web real porque todavía debemos implementar la recuperación de datos (la siguiente sección del codelab).

Sin embargo, si navegas a la pestaña Cloud Firestore de Firebase console, ahora deberías ver documentos nuevos en la colección restaurants.

img6.png

Felicitaciones, acabas de escribir datos en Cloud Firestore desde una app web.

En la siguiente sección, aprenderás a recuperar datos desde Cloud Firestore y mostrarlos en tu app.

7. Muestra datos de Cloud Firestore

En esta sección, aprenderás a recuperar datos de Cloud Firestore y mostrarlos en tu app. Los dos pasos clave son crear una consulta y agregar un objeto de escucha de instantáneas. Este objeto de escucha recibirá una notificación de todos los datos existentes que coincidan con la consulta y recibirá actualizaciones en tiempo real.

Primero, crearemos la consulta que entregará la lista de restaurantes predeterminada y sin filtros.

  1. Regresa al archivo scripts/FriendlyEats.Data.js.
  2. Busca la función FriendlyEats.prototype.getAllRestaurants.
  3. Reemplaza toda la función por el siguiente código.

FriendlyEats.Data.js

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

  this.getDocumentsInQuery(query, renderer);
};

En el código anterior, creamos una consulta que recuperará hasta 50 restaurantes de la colección de nivel superior con el nombre restaurants, ordenados según la calificación promedio (actualmente, todos en cero). Después de declarar esta consulta, la pasamos al método getDocumentsInQuery(), que es responsable de cargar y renderizar los datos.

Para ello, agregaremos un objeto de escucha de instantáneas.

  1. Regresa al archivo scripts/FriendlyEats.Data.js.
  2. Busca la función FriendlyEats.prototype.getDocumentsInQuery.
  3. Reemplaza toda la función por el siguiente código.

FriendlyEats.Data.js

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

En el código anterior, query.onSnapshot activará su devolución de llamada cada vez que haya un cambio en el resultado de la consulta.

  • La primera vez, la devolución de llamada se activa con el conjunto completo de resultados de la consulta, es decir, toda la colección restaurants de Cloud Firestore. Luego, pasa todos los documentos individuales a la función renderer.display.
  • Cuando se borra un documento, change.type equivale a removed. Por lo tanto, en este caso, llamaremos a una función que quite el restaurante de la IU.

Ahora que implementamos ambos métodos, actualiza la app y verifica que los restaurantes que vimos antes en Firebase console ahora sean visibles en la app. Si completaste esta sección correctamente, tu app ahora está leyendo y escribiendo datos con Cloud Firestore.

A medida que cambie tu lista de restaurantes, este objeto de escucha se seguirá actualizando automáticamente. Intenta ir a Firebase console y borrar manualmente un restaurante o cambiar su nombre. Los cambios aparecerán en tu sitio de inmediato.

img5.png

8. Datos get()

Hasta ahora, mostramos cómo usar onSnapshot para recuperar actualizaciones en tiempo real. Sin embargo, esto no siempre es lo que queremos. A veces, tiene más sentido recuperar los datos una sola vez.

Te recomendamos implementar un método que se active cuando un usuario haga clic en un restaurante específico de tu app.

  1. Regresa a tu archivo scripts/FriendlyEats.Data.js.
  2. Busca la función FriendlyEats.prototype.getRestaurant.
  3. Reemplaza toda la función por el siguiente código.

FriendlyEats.Data.js

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

Después de implementar este método, podrás ver las páginas de cada restaurante. Simplemente haz clic en un restaurante de la lista y deberías ver la página de detalles del restaurante:

img1.png

Por ahora, no puedes agregar calificaciones, ya que aún debemos implementarlas más adelante en el codelab.

9. Ordenar y filtrar datos

Actualmente, nuestra app muestra una lista de restaurantes, pero el usuario no tiene forma de filtrar según sus necesidades. En esta sección, usarás las consultas avanzadas de Cloud Firestore para habilitar el filtrado.

Este es un ejemplo de una consulta simple para recuperar todos los restaurantes Dim Sum:

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

Como su nombre lo indica, el método where() hará que nuestra consulta descargue solo los miembros de la colección cuyos campos cumplan con las restricciones que establecimos. En este caso, solo se descargarán restaurantes en los que category sea Dim Sum.

En nuestra app, el usuario puede encadenar varios filtros para crear consultas específicas, como “Pizza en San Francisco” o “Mariscos en Los Ángeles ordenados por popularidad”.

Crearemos un método que genere una consulta que filtrará nuestros restaurantes según varios criterios seleccionados por los usuarios.

  1. Regresa a tu archivo scripts/FriendlyEats.Data.js.
  2. Busca la función FriendlyEats.prototype.getFilteredRestaurants.
  3. Reemplaza toda la función por el siguiente código.

FriendlyEats.Data.js

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

El código anterior agrega varios filtros where y una sola cláusula orderBy para compilar una consulta compuesta basada en las entradas del usuario. Ahora, nuestra consulta solo devolverá restaurantes que coincidan con los requisitos del usuario.

Actualiza la app de FriendlyEats en el navegador y, luego, verifica que puedas filtrar por precio, ciudad y categoría. Durante la prueba, observarás errores en la Consola de JavaScript de tu navegador, que se ven de la siguiente manera:

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

Estos errores se deben a que Cloud Firestore requiere índices para la mayoría de las consultas compuestas. Exigir la indexación en las consultas mantiene la velocidad de Cloud Firestore a gran escala.

Si abres el vínculo desde el mensaje de error, se abrirá automáticamente la IU de creación de índices en Firebase console con los parámetros correctos ya completados. En la siguiente sección, escribiremos e implementaremos los índices necesarios para esta aplicación.

10. Implementa índices

Si no queremos explorar todas las rutas de acceso en nuestra app y seguir cada uno de los vínculos para crear índices, podemos implementar fácilmente muchos índices a la vez con Firebase CLI.

  1. En el directorio local descargado de tu app, encontrarás un archivo firestore.indexes.json.

Este archivo describe todos los índices necesarios para todas las combinaciones posibles de filtros.

firestore.indexes.json.

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

   ...

 ]
}
  1. Implementa estos índices con el siguiente comando:
firebase deploy --only firestore:indexes

Después de unos minutos, tus índices estarán disponibles y los mensajes de error desaparecerán.

11. Escribe datos en una transacción

En esta sección, permitiremos que los usuarios envíen opiniones a los restaurantes. Hasta ahora, todas nuestras escrituras han sido atómicas y relativamente sencillas. Si se produce un error en alguno de ellos, es probable que solo se le solicite al usuario que lo vuelva a intentar o nuestra app volverá a intentar la escritura automáticamente.

Nuestra app tendrá muchos usuarios que quieran agregar una calificación para un restaurante, por lo que tendremos que coordinar varias lecturas y escrituras. En primer lugar, se debe enviar la opinión y, luego, se deben actualizar las calificaciones count y average rating del restaurante. Si uno de estos falla, pero no el otro, nos encontramos en un estado incoherente en el que los datos de una parte de nuestra base de datos no coinciden con los datos de otra.

Afortunadamente, Cloud Firestore proporciona una funcionalidad de transacción que nos permite realizar varias lecturas y escrituras en una sola operación atómica, lo que garantiza que nuestros datos sigan siendo coherentes.

  1. Regresa a tu archivo scripts/FriendlyEats.Data.js.
  2. Busca la función FriendlyEats.prototype.addRating.
  3. Reemplaza toda la función por el siguiente código.

FriendlyEats.Data.js

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

En el bloque anterior, activamos una transacción para actualizar los valores numéricos de avgRating y numRatings en el documento del restaurante. Al mismo tiempo, agregamos la nueva rating a la subcolección ratings.

12. Protege los datos

Al comienzo de este codelab, configuramos las reglas de seguridad de nuestra app para abrir por completo la base de datos en cualquier lectura o escritura. En una aplicación real, la idea es establecer reglas mucho más detalladas para impedir el acceso o la modificación de los datos no deseados.

  1. En la sección Compilación de Firebase console, haz clic en Base de datos de Firestore.
  2. Haz clic en la pestaña Reglas en la sección Cloud Firestore (o haz clic aquí para ir directamente allí).
  3. Reemplaza los valores predeterminados por las siguientes reglas y, luego, haz clic en Publicar.

firestore.rules

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

Estas reglas restringen el acceso para garantizar que los clientes solo realicen cambios seguros. Por ejemplo:

  • Las actualizaciones de un documento de restaurante solo pueden cambiar las calificaciones, no el nombre ni otros datos inmutables.
  • Solo se pueden crear calificaciones si el ID del usuario coincide con el usuario que accedió, lo que evita la falsificación de identidad.

Además de usar Firebase console, puedes usar Firebase CLI para implementar reglas en tu proyecto de Firebase. El archivo firestore.rules de tu directorio de trabajo ya contiene las reglas anteriores. Para implementar estas reglas desde tu sistema de archivos local (en lugar de usar Firebase console), ejecuta el siguiente comando:

firebase deploy --only firestore:rules

13. Conclusión

En este codelab, aprendiste a realizar operaciones de lectura y escritura básicas y avanzadas con Cloud Firestore, y a proteger el acceso a los datos con reglas de seguridad. Puedes encontrar la solución completa en el repositorio dequickstarts-js.

Para obtener más información sobre Cloud Firestore, visita los siguientes recursos:

14. [Opcional] Aplicar de manera forzosa con la Verificación de aplicaciones

La Verificación de aplicaciones de Firebase brinda protección, ya que ayuda a validar y evitar el tráfico no deseado a tu app. En este paso, agregarás la Verificación de aplicaciones con reCAPTCHA Enterprise para proteger el acceso a tus servicios.

Primero, debes habilitar la Verificación de aplicaciones y el reCAPTCHA.

Habilitación de reCAPTCHA Enterprise

  1. En la consola de Cloud, busca y selecciona reCaptcha Enterprise en Seguridad.
  2. Habilita el servicio cuando se te solicite y haz clic en Crear clave.
  3. Ingresa un nombre visible cuando se te solicite y selecciona Sitio web como tu tipo de plataforma.
  4. Agrega las URLs que implementaste a la Lista de dominios y asegúrate de que la opción “Usar el desafío de la casilla de verificación” no esté seleccionada.
  5. Haz clic en Crear clave y almacena la clave generada en algún lugar para protegerla. Lo necesitarás más adelante en este paso.

Habilita la Verificación de aplicaciones

  1. En Firebase console, localiza la sección Compilación en el panel izquierdo.
  2. Haz clic en Verificación de aplicaciones y, luego, en el botón Comenzar (o redirecciona directamente a la consola).
  3. Haz clic en Register, ingresa tu clave de reCAPTCHA Enterprise cuando se te solicite y, luego, haz clic en Save.
  4. En la vista de APIs, selecciona Almacenamiento y haz clic en Aplicar. Haz lo mismo con Cloud Firestore.

Ahora la Verificación de aplicaciones debe aplicarse de manera forzosa. Actualiza la app y, luego, intenta crear o ver un restaurante. Deberías recibir el siguiente mensaje de error:

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

Esto significa que la Verificación de aplicaciones bloquea las solicitudes no validadas de forma predeterminada. Ahora, agreguemos la validación a tu app.

Navega al archivo FriendlyEats.View.js, actualiza la función initAppCheck y agrega tu clave de reCAPTCHA para inicializar la Verificación de aplicaciones.

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

La instancia appCheck se inicializa con un ReCaptchaEnterpriseProvider con tu clave, y isTokenAutoRefreshEnabled permite que los tokens se actualicen automáticamente en tu app.

Para habilitar las pruebas locales, busca la sección en la que se inicializa la app en el archivo FriendlyEats.js y agrega la siguiente línea a la función FriendlyEats.prototype.initAppCheck:

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

Esto registrará un token de depuración en la consola de tu aplicación web local similar al siguiente ejemplo:

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.

Ahora, ve a la vista de apps de la Verificación de aplicaciones en Firebase console.

Haz clic en el menú ampliado y selecciona Administrar tokens de depuración.

Luego, haz clic en Agregar token de depuración y pega el token de depuración de tu consola cuando se te solicite.

¡Felicitaciones! La Verificación de aplicaciones ahora debería funcionar en su aplicación.