Codelab web de Cloud Firestore

1. Descripción general

Objetivos

En este codelab, compilarás una aplicación 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 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, luego, asígnale el nombre FriendlyEats al proyecto.

Recuerda el ID de tu proyecto de Firebase.

  1. Haz clic en Crear proyecto.

La aplicación que vamos a compilar usa algunos servicios de Firebase disponibles en la Web:

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

En este codelab específico, ya configuramos Firebase Hosting. Sin embargo, en el caso de Firebase Auth y Cloud Firestore, te guiaremos a través de la configuración y habilitación de los servicios con Firebase console.

Habilitar autenticación anónima

Aunque la autenticación no es el enfoque de este codelab, es importante tener algún tipo de autenticación en nuestra app. Usaremos el acceso anónimo, lo que significa que el usuario accederá de forma silenciosa sin que se le solicite.

Debes habilitar el acceso anónimo.

  1. En Firebase console, busca la sección Build 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 forma silenciosa a tus usuarios cuando accedan a la app web. Lee la documentación sobre 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.

Las reglas de seguridad controlan el acceso a los datos en Cloud Firestore. 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 accedieron, lo que impide que los usuarios no autenticados lean o escriban. Esto es mejor que permitir el acceso público, pero aún no es 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 debe haber clonado en el directorio °friendlyeats-web. De ahora en más, asegúrate de ejecutar todos los comandos desde este directorio:

cd friendlyeats-web/vanilla-js

Importa la app de partida

Con el IDE (WebStorm, Atom, Sublime, Visual Studio Code, etc.) abre o importa el directorio °friendlyeats-web. Este directorio contiene el código de inicio para el codelab, que consiste en una app de recomendación de restaurantes que aún no es funcional. Haremos que sea funcional a lo largo de este codelab, por lo que pronto tendrás que editar el código de 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 app 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 app web para extraer la configuración de tu app para Firebase Hosting a partir de los archivos y el directorio local de tu app. Pero 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 la app con el proyecto de Firebase:
firebase use --add
  1. Cuando se te solicite, selecciona el ID del 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, usaremos el alias de default.

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

5. Ejecuta el servidor local

Ya estamos listos para comenzar a trabajar en nuestra app. Ejecutemos la app de manera local.

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

Utilizamos el emulador de Firebase Hosting para entregar nuestra 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 accediste sin aviso como usuario anónimo.

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 manualmente a través de Firebase console, pero lo haremos en la 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 dentro de cada restaurante.

img4.png

Agrega restaurantes a Firestore

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

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

Con el código anterior, se 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, add los datos.

¡Agreguemos restaurantes!

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

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

Sin embargo, si navegas a la pestaña Cloud Firestore en Firebase console, 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 de 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, creemos la consulta que entregará la lista predeterminada de restaurantes 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 restaurants, ordenados según la calificación promedio (actualmente todos 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, se activa la devolución de llamada con el conjunto de resultados completo 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 es igual a removed. Entonces, 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 se puedan ver en la app. Si completaste correctamente esta sección, la app leerá y escribirá 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 un restaurante de forma manual o cambiar su nombre. Los cambios aparecerán en tu sitio de inmediato.

img5.png

8. Datos de Get()

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

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

  1. Regresa al 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 porque aún debemos implementarlas más adelante en el codelab.

9. Cómo 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.

A continuación, se muestra 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 búsquedas específicas, como “Pizza en San Francisco” o “Mariscos en Los Ángeles ordenados por popularidad”.

Crearemos un método para generar una consulta que filtre nuestros restaurantes en función de varios criterios que seleccionen nuestros usuarios.

  1. Regresa al 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á los 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 pruebas, verá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 índices en las consultas mantiene la velocidad de Cloud Firestore a gran escala.

Si abres el vínculo del mensaje de error, se abrirá automáticamente la IU para crear í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 de creación de índices, podemos implementar fácilmente muchos índices a la vez con Firebase CLI.

  1. En el directorio local descargado de la 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 activos y los mensajes de error desaparecerán.

11. Escribe 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 escrituras han sido atómicas y relativamente simples. Si alguna de ellas falla, es probable que solo le solicitemos al usuario que vuelva a intentarlo, o nuestra app volverá a intentar la escritura automáticamente.

Nuestra app tendrá muchos usuarios que desean 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 en sí y, luego, se deben actualizar las calificaciones count y average rating del restaurante. Si uno de estos falla, pero no el otro, nos quedamos en un estado incoherente: 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 se mantengan coherentes.

  1. Regresa al 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 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, es necesario establecer reglas mucho más precisas 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, pero 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.

Como alternativa a Firebase console, puedes usar Firebase CLI para implementar reglas en tu proyecto de Firebase. El archivo firestore.rules en 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), debes ejecutar el siguiente comando:

firebase deploy --only firestore:rules

13. Conclusión

En este codelab, aprendiste a realizar lecturas y escrituras 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, consulta los siguientes recursos:

14. [Opcional] Aplicar la Verificación de aplicaciones 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 a reCAPTCHA Enterprise para proteger el acceso a tus servicios.

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

Habilitación de reCAPTCHA Enterprise

  1. En la consola de Cloud, busca y selecciona reCaptcha Enterprise en Seguridad.
  2. Habilita el servicio como se solicita y haz clic en Crear clave.
  3. Ingresa un nombre visible cuando se solicite y selecciona Sitio web como el tipo de plataforma.
  4. Agrega las 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” no esté seleccionada.
  5. Haz clic en Crear clave y guarda la clave generada en algún lugar para guardarla de forma segura. Lo 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 Register, ingresa tu clave de reCAPTCHA Enterprise cuando se te solicite y, luego, haz clic en Guardar.
  4. En la vista de APIs, selecciona Storage y haz clic en Aplicar. Haz lo mismo con Cloud Firestore.

Ahora se debe aplicar la Verificación de aplicaciones. Actualiza la app y trata de 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 validación a tu app.

Navega al archivo FriendlyEats.View.js, actualiza la función initAppCheck y agrega la clave 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 la 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:

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 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 como se te solicite.

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