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

  • Cómo leer y escribir datos en Cloud Firestore desde una app web
  • Cómo detectar cambios en datos de Cloud Firestore en tiempo real
  • Cómo usar Firebase Authentication y reglas de seguridad para proteger datos de Cloud Firestore
  • Cómo escribir consultas complejas de Cloud Firestore

Requisitos

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

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 actualizan los datos
  • Firebase Hosting para alojar y entregar tus recursos estáticos

Para este codelab específico, ya configuramos Firebase Hosting. Sin embargo, en el caso de Firebase Auth y Cloud Firestore, te explicaremos cómo configurar y habilitar los servicios con Firebase console.

Habilitar la autenticación anónima

Si bien la autenticación no es el enfoque de este codelab, es importante tener alguna forma de autenticación en nuestra app. Usaremos acceso anónimo, es decir, 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 les brinde, de manera silenciosa, acceso a tus usuarios cuando ingresen a la aplicación web. Para obtener más información, consulta la documentación sobre autenticación anónima.

Habilita Cloud Firestore

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

Deberás habilitar Cloud Firestore. En la sección Build 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

Con tu IDE (WebStorm, Atom, Sublime, Visual Studio Code, etc.), abre o importa el directorio 📁friendlyeats-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. Para instalar la CLI, ejecuta el siguiente comando npm:
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 v7.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 Firebase Hosting de tu app del directorio y los archivos locales. 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 la app.
  2. Ejecuta el siguiente comando para asociar tu 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 la línea de comandos.

5. Ejecuta el servidor local

Estamos listos para comenzar a trabajar en nuestra app. Ejecutemos nuestra app de forma local.

  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 app 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 consisten en 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, creemos 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, construimos una consulta que recuperará hasta 50 restaurantes de la colección de nivel superior llamada restaurants, que se ordenan por la calificación promedio (en este momento, 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. 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 se puedan observar en la app. Si completaste esta sección con éxito, tu app ahora lee y escribe datos con Cloud Firestore.

A medida que cambie tu lista de restaurantes, este objeto de escucha se seguirá actualizando automáticamente. Prueba ir a Firebase console y borrar, de forma manual, un restaurante o cambiar su nombre. Observarás que los cambios aparecen en el sitio de inmediato.

img5.png

8. Datos get()

Hasta ahora, mostramos cómo usar onSnapshot para recuperar actualizaciones en tiempo real. pero eso no siempre es lo que queremos. A veces, tiene más sentido solo recuperar los datos una 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. Ordena y filtra 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 configuramos. 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 de Los Ángeles ordenados por popularidad".

Crearemos un método que compile una consulta que filtre nuestros restaurantes según varios criterios que seleccionen nuestros 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 mostrará 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. Mientras realizas las pruebas, 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. Cómo implementar índices

Si no queremos explorar todas las rutas de 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, se publicarán tus índices y desaparecerán los mensajes de error.

11. Cómo escribir datos en una transacción

En esta sección, agregaremos la capacidad de que los usuarios envíen opiniones a los restaurantes. Hasta ahora, todas nuestras operaciones de escritura 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 vuelva a intentarlo 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. Primero, 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.

Por suerte, Cloud Firestore proporciona una funcionalidad de transacción que nos permite realizar múltiples 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 el nuevo rating a la subcolección ratings.

12. Protege los datos

Al principio de este codelab, configuramos las reglas de seguridad de nuestra app para abrir la base de datos por completo en cualquier operación de lectura o escritura. En una aplicación real, querríamos establecer reglas mucho más detalladas para evitar el acceso o la modificación de 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 de quickstarts-js.

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

14. [Opcional] Aplica la política 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.

Cómo habilitar reCaptcha Enterprise

  1. En la consola de Cloud, busca y selecciona reCAPTCHA Enterprise en Seguridad.
  2. Habilita el servicio según se te solicite y haz clic en Create Key.
  3. Ingresa un nombre visible según se te solicite y selecciona Sitio web como el tipo de plataforma.
  4. Agrega tus URLs implementadas a la lista de dominios y asegúrate de que la opción “Usar el desafío de la casilla de verificación” esté sin seleccionar.
  5. Haz clic en Create Key y almacena la clave generada en un lugar seguro. La necesitarás más adelante en este paso.

Habilita la Verificación de aplicaciones

  1. En Firebase console, busca la sección Build 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 Registrar y, cuando se te solicite, ingresa tu clave de reCaptcha Enterprise. Luego, haz clic en Guardar.
  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 prueba 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 de 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.