Creación de una aplicación de notificación de precios de acciones con React, Apollo GraphQL y Hasura

Publicado: 2022-03-10
Resumen rápido ↬ En este artículo, aprenderemos cómo crear una aplicación basada en eventos y enviar una notificación push web cuando se activa un evento en particular. Configuraremos tablas de base de datos, eventos y disparadores programados en el motor Hasura GraphQL y conectaremos el punto final de GraphQL a la aplicación frontal para registrar la preferencia de precio de las acciones del usuario.

El concepto de recibir una notificación cuando ocurre el evento de su elección se ha vuelto popular en comparación con estar pegado al flujo continuo de datos para encontrar ese evento en particular usted mismo. Las personas prefieren recibir correos electrónicos/mensajes relevantes cuando ha ocurrido su evento preferido en lugar de estar enganchados en la pantalla para esperar a que suceda ese evento. La terminología basada en eventos también es bastante común en el mundo del software.

¿Qué tan maravilloso sería si pudieras obtener las actualizaciones del precio de tus acciones favoritas en tu teléfono?

En este artículo, vamos a crear una aplicación de notificador de precios de acciones utilizando los motores React, Apollo GraphQL y Hasura GraphQL. Vamos a comenzar el proyecto a partir de un código repetitivo de create-react-app y construiremos todo desde cero. Aprenderemos a configurar las tablas de la base de datos y los eventos en la consola de Hasura. También aprenderemos cómo conectar los eventos de Hasura para obtener actualizaciones del precio de las acciones mediante notificaciones web.

Aquí hay un vistazo rápido a lo que estaríamos construyendo:

Descripción general de la aplicación Notificador de precios de acciones
Aplicación de notificador de precios de acciones

¡Vámonos!

¡Más después del salto! Continúe leyendo a continuación ↓

Una descripción general de lo que trata este proyecto

Los datos de acciones (incluidas métricas como alto , bajo , abierto , cerrado , volumen ) se almacenarían en una base de datos Postgres respaldada por Hasura. El usuario podría suscribirse a una acción en particular en función de algún valor o puede optar por recibir una notificación cada hora. El usuario recibirá una notificación push web una vez que se cumplan sus criterios de suscripción.

Esto parece un montón de cosas y obviamente habría algunas preguntas abiertas sobre cómo construiremos estas piezas.

Aquí hay un plan sobre cómo lograríamos este proyecto en cuatro pasos:

  1. Obtener los datos de acciones usando un script de NodeJs
    Comenzaremos por obtener los datos de acciones utilizando un script simple de NodeJs de uno de los proveedores de API de acciones: Alpha Vantage. Este script obtendrá los datos de una acción en particular en intervalos de 5 minutos. La respuesta de la API incluye alta , baja , apertura , cierre y volumen . Estos datos se insertarán luego en la base de datos de Postgres que está integrada con el back-end de Hasura.
  2. Configuración del motor Hasura GraphQL
    Luego configuraremos algunas tablas en la base de datos de Postgres para registrar puntos de datos. Hasura genera automáticamente los esquemas, consultas y mutaciones de GraphQL para estas tablas.
  3. Front-end usando React y Apollo Client
    El siguiente paso es integrar la capa GraphQL utilizando el cliente Apollo y el proveedor Apollo (el punto final de GraphQL proporcionado por Hasura). Los puntos de datos se mostrarán como gráficos en el front-end. También crearemos las opciones de suscripción y activaremos las mutaciones correspondientes en la capa GraphQL.
  4. Configuración de activadores de eventos/programados
    Hasura proporciona una excelente herramienta en torno a los disparadores. Estaremos agregando activadores de eventos y programados en la tabla de datos de acciones. Estos disparadores se establecerán si el usuario está interesado en recibir una notificación cuando los precios de las acciones alcancen un valor particular (disparador de eventos). El usuario también puede optar por recibir una notificación de una acción en particular cada hora (disparador programado).

Ahora que el plan está listo, ¡pongámoslo en acción!

Aquí está el repositorio de GitHub para este proyecto. Si se pierde en alguna parte del código a continuación, consulte este repositorio y vuelva a la velocidad.

Obtener los datos de acciones usando un script de NodeJs

¡Esto no es tan complicado como parece! Tendremos que escribir una función que obtenga datos usando el punto final de Alpha Vantage y esta llamada de búsqueda debe activarse en un intervalo de 5 minutos (Lo adivinó bien, tendremos que poner esta llamada de función en setInterval ).

Si todavía se pregunta qué es Alpha Vantage y solo quiere quitarse esto de la cabeza antes de saltar a la parte de codificación, aquí está:

Alpha Vantage Inc. es un proveedor líder de API gratuitas para datos históricos y en tiempo real sobre acciones, divisas (FX) y criptomonedas/digitales.

Estaríamos usando este punto final para obtener las métricas requeridas de una acción en particular. Esta API espera una clave de API como uno de los parámetros. Puede obtener su clave de API gratuita desde aquí. Ahora estamos listos para pasar a la parte interesante: ¡comencemos a escribir algo de código!

Instalación de dependencias

Cree un directorio stocks-app y cree un directorio de server dentro de él. Inicialízalo como un proyecto de nodo usando npm init y luego instala estas dependencias:

 npm i isomorphic-fetch pg nodemon --save

Estas son las únicas tres dependencias que necesitaríamos para escribir este script para obtener los precios de las acciones y almacenarlos en la base de datos de Postgres.

He aquí una breve explicación de estas dependencias:

  • isomorphic-fetch
    Facilita el uso de fetch isomórficamente (en la misma forma) tanto en el cliente como en el servidor.
  • pg
    Es un cliente PostgreSQL sin bloqueo para NodeJs.
  • nodemon
    Reinicia automáticamente el servidor en cualquier cambio de archivo en el directorio.

Estableciendo la configuración

Agregue un archivo config.js en el nivel raíz. Agregue el siguiente fragmento de código en ese archivo por ahora:

 const config = { user: '<DATABASE_USER>', password: '<DATABASE_PASSWORD>', host: '<DATABASE_HOST>', port: '<DATABASE_PORT>', database: '<DATABASE_NAME>', ssl: '<IS_SSL>', apiHost: 'https://www.alphavantage.co/', }; module.exports = config;

El user , password , host , port , base de database , ssl están relacionados con la configuración de Postgres. ¡Volveremos a editar esto mientras configuramos la parte del motor Hasura!

Inicializar el grupo de conexiones de Postgres para consultar la base de datos

Un connection pool es un término común en informática y, a menudo, escuchará este término al tratar con bases de datos.

Al consultar datos en bases de datos, primero deberá establecer una conexión con la base de datos. Esta conexión toma las credenciales de la base de datos y le brinda un enlace para consultar cualquiera de las tablas en la base de datos.

Nota : Establecer conexiones de base de datos es costoso y también desperdicia recursos significativos. Un grupo de conexiones almacena en caché las conexiones de la base de datos y las reutiliza en consultas posteriores. Si todas las conexiones abiertas están en uso, se establece una nueva conexión y luego se agrega al grupo.

Ahora que está claro qué es el grupo de conexiones y para qué se usa, comencemos creando una instancia del grupo de conexiones pg para esta aplicación:

Agregue el archivo pool.js en el nivel raíz y cree una instancia de grupo como:

 const { Pool } = require('pg'); const config = require('./config'); const pool = new Pool({ user: config.user, password: config.password, host: config.host, port: config.port, database: config.database, ssl: config.ssl, }); module.exports = pool;

Las líneas de código anteriores crean una instancia de Pool con las opciones de configuración establecidas en el archivo de configuración. Todavía tenemos que completar el archivo de configuración, pero no habrá cambios relacionados con las opciones de configuración.

Ahora hemos sentado las bases y estamos listos para comenzar a realizar algunas llamadas de API al punto final de Alpha Vantage.

¡Pasemos a lo interesante!

Obtener los datos de las acciones

En esta sección, buscaremos los datos de stock del punto final de Alpha Vantage. Aquí está el archivo index.js :

 const fetch = require('isomorphic-fetch'); const getConfig = require('./config'); const { insertStocksData } = require('./queries'); const symbols = [ 'NFLX', 'MSFT', 'AMZN', 'W', 'FB' ]; (function getStocksData () { const apiConfig = getConfig('apiHostOptions'); const { host, timeSeriesFunction, interval, key } = apiConfig; symbols.forEach((symbol) => { fetch(`${host}query/?function=${timeSeriesFunction}&symbol=${symbol}&interval=${interval}&apikey=${key}`) .then((res) => res.json()) .then((data) => { const timeSeries = data['Time Series (5min)']; Object.keys(timeSeries).map((key) => { const dataPoint = timeSeries[key]; const payload = [ symbol, dataPoint['2. high'], dataPoint['3. low'], dataPoint['1. open'], dataPoint['4. close'], dataPoint['5. volume'], key, ]; insertStocksData(payload); }); }); }) })()

A los efectos de este proyecto, vamos a consultar los precios solo de estas acciones: NFLX (Netflix), MSFT (Microsoft), AMZN (Amazon), W (Wayfair), FB (Facebook).

Consulte este archivo para ver las opciones de configuración. ¡La función getStocksData de getStocksData no está haciendo mucho! Recorre estos símbolos y consulta el punto final de Alpha Vantage ${host}query/?function=${timeSeriesFunction}&symbol=${symbol}&interval=${interval}&apikey=${key} para obtener las métricas de estas acciones.

La función insertStocksData coloca estos puntos de datos en la base de datos de Postgres. Aquí está la función insertStocksData :

 const insertStocksData = async (payload) => { const query = 'INSERT INTO stock_data (symbol, high, low, open, close, volume, time) VALUES ($1, $2, $3, $4, $5, $6, $7)'; pool.query(query, payload, (err, result) => { console.log('result here', err); }); };

¡Eso es todo! Obtuvimos puntos de datos del stock de la API de Alpha Vantage y escribimos una función para colocarlos en la base de datos de Postgres en la tabla stock_data . ¡Solo falta una pieza para que todo esto funcione! Tenemos que completar los valores correctos en el archivo de configuración. Obtendremos estos valores después de configurar el motor Hasura. ¡Vamos a eso de inmediato!

Consulte el directorio del server para obtener el código completo sobre cómo obtener puntos de datos del punto final de Alpha Vantage y completarlos en la base de datos de Hasura Postgres.

Si este enfoque de establecer conexiones, opciones de configuración e insertar datos usando la consulta sin procesar parece un poco difícil, ¡no se preocupe por eso! ¡Vamos a aprender cómo hacer todo esto de la manera más fácil con una mutación de GraphQL una vez que el motor Hasura esté configurado!

Configuración del motor Hasura GraphQL

¡Es realmente simple configurar el motor Hasura y ponerlo en funcionamiento con los esquemas, consultas, mutaciones, suscripciones, activadores de eventos de GraphQL y mucho más!

Haga clic en Probar Hasura e ingrese el nombre del proyecto:

Creación de un proyecto Hasura
Creación de un proyecto Hasura. (Vista previa grande)

Estoy usando la base de datos Postgres alojada en Heroku. Cree una base de datos en Heroku y vincúlela a este proyecto. Entonces debería estar listo para experimentar el poder de la consola Hasura rica en consultas.

Copie la URL de la base de datos de Postgres que obtendrá después de crear el proyecto. Tendremos que poner esto en el archivo de configuración.

Haga clic en Iniciar consola y será redirigido a esta vista:

Consola Hasura
Consola Hasura. (Vista previa grande)

Comencemos a construir el esquema de tabla que necesitaríamos para este proyecto.

Creación de un esquema de tablas en la base de datos de Postgres

Vaya a la pestaña Datos y haga clic en Agregar tabla. Empecemos a crear algunas de las tablas:

tabla de symbol

Esta tabla se usaría para almacenar la información de los símbolos. Por ahora, mantuve dos campos aquí: id y company . El campo id es una clave principal y la company es de tipo varchar . Agreguemos algunos de los símbolos en esta tabla:

tabla de símbolos
tabla de symbol (Vista previa grande)

tabla de stock_data

La tabla stock_data almacena id , symbol , time y métricas como high , low , open , close , volume . El script de NodeJs que escribimos anteriormente en esta sección se usará para completar esta tabla en particular.

Así es como se ve la tabla:

tabla de stock_data
tabla stock_data . (Vista previa grande)

¡Limpio! ¡Pasemos a la otra tabla en el esquema de la base de datos!

tabla de user_subscription

La tabla user_subscription almacena el objeto de suscripción contra el ID de usuario. Este objeto de suscripción se utiliza para enviar notificaciones push web a los usuarios. Aprenderemos más adelante en el artículo cómo generar este objeto de suscripción.

Hay dos campos en esta tabla: id es la clave principal de tipo uuid y el campo de suscripción es de tipo jsonb .

tabla de events

Este es el importante y se utiliza para almacenar las opciones de eventos de notificación. Cuando un usuario opta por las actualizaciones de precios de una acción en particular, almacenamos la información de ese evento en esta tabla. Esta tabla contiene estas columnas:

  • id : es una clave principal con la propiedad de incremento automático.
  • symbol : es un campo de texto.
  • user_id : es de tipo uuid .
  • trigger_type : se utiliza para almacenar el tipo de desencadenante del evento: time/event .
  • trigger_value : se utiliza para almacenar el valor de activación. Por ejemplo, si un usuario ha optado por activar un evento basado en el precio (quiere actualizaciones si el precio de la acción ha llegado a 1000), entonces trigger_value sería 1000 y trigger_type sería event .

Estas son todas las tablas que necesitaríamos para este proyecto. También tenemos que establecer relaciones entre estas tablas para tener un flujo de datos y conexiones fluidos. ¡Vamos a hacer eso!

Establecer relaciones entre tablas

La tabla de events se utiliza para enviar notificaciones push web basadas en el valor del evento. Por lo tanto, tiene sentido conectar esta tabla con la tabla user_subscription para poder enviar notificaciones automáticas sobre las suscripciones almacenadas en esta tabla.

 events.user_id → user_subscription.id

La tabla stock_data está relacionada con la tabla de símbolos como:

 stock_data.symbol → symbol.id

También tenemos que construir algunas relaciones en la tabla de symbol como:

 stock_data.symbol → symbol.id events.symbol → symbol.id

¡Ya hemos creado las tablas requeridas y también hemos establecido las relaciones entre ellas! ¡Vamos a cambiar a la pestaña GRAPHIQL en la consola para ver la magia!

Hasura ya ha configurado las consultas GraphQL basadas en estas tablas:

Consultas/mutaciones de GraphQL en la consola de Hasura
Consultas/mutaciones de GraphQL en la consola de Hasura. (Vista previa grande)

Es claramente simple consultar estas tablas y también puede aplicar cualquiera de estos filtros/propiedades ( distinct_on , limit , offset , order_by , where ) para obtener los datos deseados.

Todo esto se ve bien, pero aún no hemos conectado nuestro código del lado del servidor a la consola de Hasura. ¡Completamos ese bit!

Conexión del script de NodeJs a la base de datos de Postgres

Coloque las opciones requeridas en el archivo config.js en el directorio del server como:

 const config = { databaseOptions: { user: '<DATABASE_USER>', password: '<DATABASE_PASSWORD>', host: '<DATABASE_HOST>', port: '<DATABASE_PORT>', database: '<DATABASE_NAME>', ssl: true, }, apiHostOptions: { host: 'https://www.alphavantage.co/', key: '<API_KEY>', timeSeriesFunction: 'TIME_SERIES_INTRADAY', interval: '5min' }, graphqlURL: '<GRAPHQL_URL>' }; const getConfig = (key) => { return config[key]; }; module.exports = getConfig;

Coloque estas opciones de la cadena de la base de datos que se generó cuando creamos la base de datos de Postgres en Heroku.

apiHostOptions consta de las opciones relacionadas con la API, como host , key , timeSeriesFunction e interval .

Obtendrá el campo graphqlURL en la pestaña GRAPHIQL en la consola de Hasura.

La función getConfig se usa para devolver el valor solicitado del objeto de configuración. Ya hemos usado esto en index.js en el directorio del server .

Es hora de ejecutar el servidor y completar algunos datos en la base de datos. Agregué un script en package.json como:

 "scripts": { "start": "nodemon index.js" }

Ejecute npm start en el terminal y los puntos de datos de la matriz de símbolos en index.js deben completarse en las tablas.

Refactorización de la consulta sin procesar en el script de NodeJs a la mutación de GraphQL

Ahora que el motor Hasura está configurado, veamos qué tan fácil puede ser llamar a una mutación en la tabla stock_data .

La función insertStocksData en queries.js usa una consulta sin procesar:

 const query = 'INSERT INTO stock_data (symbol, high, low, open, close, volume, time) VALUES ($1, $2, $3, $4, $5, $6, $7)';

Refactoricemos esta consulta y usemos la mutación impulsada por el motor Hasura. Aquí está el queries.js refactorizado en el directorio del servidor:

 const { createApolloFetch } = require('apollo-fetch'); const getConfig = require('./config'); const GRAPHQL_URL = getConfig('graphqlURL'); const fetch = createApolloFetch({ uri: GRAPHQL_URL, }); const insertStocksData = async (payload) => { const insertStockMutation = await fetch({ query: `mutation insertStockData($objects: [stock_data_insert_input!]!) { insert_stock_data (objects: $objects) { returning { id } } }`, variables: { objects: payload, }, }); console.log('insertStockMutation', insertStockMutation); }; module.exports = { insertStocksData }

Tenga en cuenta: tenemos que agregar graphqlURL en el archivo config.js .

El módulo apollo-fetch devuelve una función de búsqueda que se puede usar para consultar/mutar la fecha en el punto final de GraphQL. Bastante fácil, ¿verdad?

El único cambio que tenemos que hacer en index.js es devolver el objeto de acciones en el formato requerido por la función insertStocksData . Consulte index2.js y queries2.js para obtener el código completo con este enfoque.

Ahora que hemos logrado el lado de los datos del proyecto, pasemos a la parte frontal y construyamos algunos componentes interesantes.

Nota : ¡No tenemos que mantener las opciones de configuración de la base de datos con este enfoque!

Front-end usando React y Apollo Client

El proyecto front-end está en el mismo repositorio y se crea usando el paquete create-react-app . El trabajador de servicio generado con este paquete admite el almacenamiento en caché de activos, pero no permite que se agreguen más personalizaciones al archivo del trabajador de servicio. Ya hay algunos problemas abiertos para agregar soporte para opciones de trabajadores de servicios personalizados. Hay formas de solucionar este problema y agregar soporte para un trabajador de servicio personalizado.

Comencemos mirando la estructura del proyecto front-end:

Directorio de proyectos
Directorio de proyectos. (Vista previa grande)

¡Consulte el directorio src ! No se preocupe por los archivos relacionados con el trabajador del servicio por ahora. Aprenderemos más sobre estos archivos más adelante en esta sección. El resto de la estructura del proyecto parece simple. La carpeta de components tendrá los componentes (Cargador, Gráfico); la carpeta de services contiene algunas de las funciones/servicios auxiliares utilizados para transformar objetos en la estructura requerida; styles , como sugiere el nombre, contiene los archivos sass utilizados para diseñar el proyecto; views es el directorio principal y contiene los componentes de la capa de vista.

Necesitaríamos solo dos componentes de vista para este proyecto: la lista de símbolos y la serie temporal de símbolos. Construiremos la serie de tiempo utilizando el componente Gráfico de la biblioteca de gráficos altos. ¡Comencemos a agregar código en estos archivos para construir las piezas en el front-end!

Instalación de dependencias

Aquí está la lista de dependencias que necesitaremos:

  • apollo-boost
    Apollo boost es una forma de configuración cero para comenzar a usar Apollo Client. Viene incluido con las opciones de configuración predeterminadas.
  • reactstrap de reacción y bootstrap
    Los componentes se construyen utilizando estos dos paquetes.
  • graphql y graphql-type-json
    graphql es una dependencia requerida para usar apollo-boost y graphql-type-json se usa para admitir el tipo de datos json que se usa en el esquema de GraphQL.
  • highcharts y highcharts-react-official
    Y estos dos paquetes se utilizarán para construir el gráfico:

  • node-sass
    Esto se agrega para admitir archivos sass para diseñar.

  • uuid
    Este paquete se utiliza para generar valores aleatorios fuertes.

Todas estas dependencias tendrán sentido una vez que comencemos a usarlas en el proyecto. ¡Pasemos a la siguiente parte!

Configuración del cliente Apollo

Cree un apolloClient.js dentro de la carpeta src como:

 import ApolloClient from 'apollo-boost'; const apolloClient = new ApolloClient({ uri: '<HASURA_CONSOLE_URL>' }); export default apolloClient;

El código anterior crea una instancia de ApolloClient y toma uri en las opciones de configuración. El uri es la URL de su consola Hasura. Obtendrá este campo uri en la pestaña GRAPHIQL en la sección Punto final de GraphQL.

¡El código anterior parece simple pero se ocupa de la parte principal del proyecto! Conecta el esquema GraphQL creado en Hasura con el proyecto actual.

También tenemos que pasar este objeto de cliente de apolo a ApolloProvider y envolver el componente raíz dentro ApolloProvider . Esto permitirá que todos los componentes anidados dentro del componente principal usen la propiedad del client y activen consultas en este objeto del cliente.

Modifiquemos el archivo index.js como:

 const Wrapper = () => { /* some service worker logic - ignore for now */ const [insertSubscription] = useMutation(subscriptionMutation); useEffect(() => { serviceWorker.register(insertSubscription); }, []) /* ignore the above snippet */ return <App />; } ReactDOM.render( <ApolloProvider client={apolloClient}> <Wrapper /> </ApolloProvider>, document.getElementById('root') );

Ignore el código relacionado con insertSubscription . Lo entenderemos en detalle más adelante. El resto del código debería ser fácil de manejar. La función render toma el componente raíz y el elementId como parámetros. Observe client (instancia de ApolloClient) se pasa como accesorio a ApolloProvider . Puede consultar el archivo index.js completo aquí.

Configuración del trabajador de servicio personalizado

Un trabajador de servicio es un archivo JavaScript que tiene la capacidad de interceptar solicitudes de red. Se utiliza para consultar el caché para verificar si el activo solicitado ya está presente en el caché en lugar de realizar un viaje al servidor. Los trabajadores del servicio también se utilizan para enviar notificaciones push web a los dispositivos suscritos.

Tenemos que enviar notificaciones web push para las actualizaciones del precio de las acciones a los usuarios suscritos. ¡Preparamos el terreno y construyamos este archivo de trabajador de servicio!

El recorte relacionado con insertSubscription en el archivo index.js está haciendo el trabajo de registrar al trabajador del servicio y colocar el objeto de suscripción en la base de datos usando subscriptionMutation .

Consulte queries.js para conocer todas las consultas y mutaciones que se utilizan en el proyecto.

serviceWorker.register(insertSubscription); invoca la función de register escrita en el archivo serviceWorker.js . Aquí lo tienes:

 export const register = (insertSubscription) => { if ('serviceWorker' in navigator) { const swUrl = `${process.env.PUBLIC_URL}/serviceWorker.js` navigator.serviceWorker.register(swUrl) .then(() => { console.log('Service Worker registered'); return navigator.serviceWorker.ready; }) .then((serviceWorkerRegistration) => { getSubscription(serviceWorkerRegistration, insertSubscription); Notification.requestPermission(); }) } }

La función anterior primero verifica si serviceWorker es compatible con el navegador y luego registra el archivo del trabajador del servicio alojado en la URL swUrl . ¡Revisaremos este archivo en un momento!

La función getSubscription hace el trabajo de obtener el objeto de suscripción utilizando el método de subscribe en el objeto pushManager . Este objeto de suscripción luego se almacena en la tabla user_subscription contra un ID de usuario. Tenga en cuenta que el ID de usuario se genera mediante la función uuid . Veamos la función getSubscription :

 const getSubscription = (serviceWorkerRegistration, insertSubscription) => { serviceWorkerRegistration.pushManager.getSubscription() .then ((subscription) => { const userId = uuidv4(); if (!subscription) { const applicationServerKey = urlB64ToUint8Array('<APPLICATION_SERVER_KEY>') serviceWorkerRegistration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey }).then (subscription => { insertSubscription({ variables: { userId, subscription } }); localStorage.setItem('serviceWorkerRegistration', JSON.stringify({ userId, subscription })); }) } }) }

¡Puede consultar el archivo serviceWorker.js para obtener el código completo!

Ventana emergente de notificación
Ventana emergente de notificación. (Vista previa grande)

Notification.requestPermission() invocó esta ventana emergente que le pide al usuario permiso para enviar notificaciones. Una vez que el usuario hace clic en Permitir, el servicio de inserción genera un objeto de suscripción. Estamos almacenando ese objeto en localStorage como:

Objeto de suscripciones Webpush
Objeto de suscripciones Webpush. (Vista previa grande)

El endpoint del campo en el objeto anterior se usa para identificar el dispositivo y el servidor usa este punto final para enviar notificaciones push web al usuario.

Hemos hecho el trabajo de inicializar y registrar el service worker. ¡También tenemos el objeto de suscripción del usuario! Esto funciona bien debido al archivo serviceWorker.js presente en la carpeta public . ¡Configuremos ahora el service worker para preparar todo!

Este es un tema un poco difícil, ¡pero hagámoslo bien! Como se mencionó anteriormente, la utilidad create-react-app no ​​admite personalizaciones de forma predeterminada para el trabajador del servicio. Podemos lograr la implementación del trabajador de servicio al cliente utilizando el módulo workbox-build .

También debemos asegurarnos de que el comportamiento predeterminado de los archivos de almacenamiento previo en caché esté intacto. Modificaremos la parte donde se construye el trabajador de servicio en el proyecto. ¡Y workbox-build ayuda a lograr exactamente eso! ¡Cosas ordenadas! Simplifiquemos las cosas y enumeremos todo lo que tenemos que hacer para que el trabajador del servicio personalizado funcione:

  • Maneje el almacenamiento previo en caché de los activos mediante workboxBuild .
  • Cree una plantilla de trabajador de servicio para almacenar activos en caché.
  • Cree el archivo sw-precache-config.js para proporcionar opciones de configuración personalizadas.
  • Agregue la secuencia de comandos del trabajador del servicio de compilación en el paso de compilación en package.json .

¡No se preocupe si todo esto suena confuso! El artículo no se enfoca en explicar la semántica detrás de cada uno de estos puntos. ¡Tenemos que centrarnos en la parte de implementación por ahora! Trataré de cubrir el razonamiento detrás de hacer todo el trabajo para hacer un trabajador de servicio personalizado en otro artículo.

Vamos a crear dos archivos sw-build.js y sw-custom.js en el directorio src . Consulte los enlaces a estos archivos y agregue el código a su proyecto.

Ahora creemos el archivo sw-precache-config.js en el nivel raíz y agreguemos el siguiente código en ese archivo:

 module.exports = { staticFileGlobs: [ 'build/static/css/**.css', 'build/static/js/**.js', 'build/index.html' ], swFilePath: './build/serviceWorker.js', stripPrefix: 'build/', handleFetch: false, runtimeCaching: [{ urlPattern: /this\\.is\\.a\\.regex/, handler: 'networkFirst' }] }

También modifiquemos el archivo package.json para hacer espacio para construir el archivo de trabajador de servicio personalizado:

Agregue estas declaraciones en la sección de scripts :

 "build-sw": "node ./src/sw-build.js", "clean-cra-sw": "rm -f build/precache-manifest.*.js && rm -f build/service-worker.js",

Y modifique el script de build como:

 "build": "react-scripts build && npm run build-sw && npm run clean-cra-sw",

¡La configuración finalmente está lista! Ahora tenemos que agregar un archivo de trabajador de servicio personalizado dentro de la carpeta public :

 function showNotification (event) { const eventData = event.data.json(); const { title, body } = eventData self.registration.showNotification(title, { body }); } self.addEventListener('push', (event) => { event.waitUntil(showNotification(event)); })

Acabamos de agregar un oyente de push para escuchar las notificaciones de inserción que envía el servidor. La función showNotification se utiliza para mostrar notificaciones push web al usuario.

¡Eso es todo! Hemos terminado con todo el arduo trabajo de configurar un trabajador de servicio personalizado para manejar las notificaciones push web. ¡Veremos estas notificaciones en acción una vez que construyamos las interfaces de usuario!

Nos estamos acercando a la construcción de las piezas principales del código. ¡Comencemos ahora con la primera vista!

Vista de lista de símbolos

El componente de la App que se usa en la sección anterior se ve así:

 import React from 'react'; import SymbolList from './views/symbolList'; const App = () => { return <SymbolList />; }; export default App;

Es un componente simple que devuelve la vista de SymbolList y SymbolList hace todo el trabajo pesado de mostrar símbolos en una interfaz de usuario ordenada.

Veamos symbolList.js dentro de la carpeta de views :

¡Consulte el archivo aquí!

El componente devuelve los resultados de la función renderSymbols . Y estos datos se obtienen de la base de datos utilizando el gancho useQuery como:

 const { loading, error, data } = useQuery(symbolsQuery, {variables: { userId }});

La consulta de symbolsQuery se define como:

 export const symbolsQuery = gql` query getSymbols($userId: uuid) { symbol { id company symbol_events(where: {user_id: {_eq: $userId}}) { id symbol trigger_type trigger_value user_id } stock_symbol_aggregate { aggregate { max { high volume } min { low volume } } } } } `;

Toma el ID de userId y obtiene los eventos suscritos de ese usuario en particular para mostrar el estado correcto del icono de notificación (icono de campana que se muestra junto con el título). La consulta también obtiene los valores máximo y mínimo del stock. Observe el uso de aggregate en la consulta anterior. Las consultas de agregación de Hasura hacen el trabajo detrás de escena para obtener los valores agregados como count , sum , avg , max , min , etc.

Según la respuesta de la llamada de GraphQL anterior, aquí está la lista de tarjetas que se muestran en el front-end:

Tarjetas de existencias
Tarjetas de existencias. (Vista previa grande)

La estructura HTML de la tarjeta se parece a esto:

 <div key={id}> <div className="card-container"> <Card> <CardBody> <CardTitle className="card-title"> <span className="company-name">{company} </span> <Badge color="dark" pill>{id}</Badge> <div className={classNames({'bell': true, 'disabled': isSubscribed})} id={`subscribePopover-${id}`}> <FontAwesomeIcon icon={faBell} title="Subscribe" /> </div> </CardTitle> <div className="metrics"> <div className="metrics-row"> <span className="metrics-row--label">High:</span> <span className="metrics-row--value">{max.high}</span> <span className="metrics-row--label">{' '}(Volume: </span> <span className="metrics-row--value">{max.volume}</span>) </div> <div className="metrics-row"> <span className="metrics-row--label">Low: </span> <span className="metrics-row--value">{min.low}</span> <span className="metrics-row--label">{' '}(Volume: </span> <span className="metrics-row--value">{min.volume}</span>) </div> </div> <Button className="timeseries-btn" outline onClick={() => toggleTimeseries(id)}>Timeseries</Button>{' '} </CardBody> </Card> <Popover className="popover-custom" placement="bottom" target={`subscribePopover-${id}`} isOpen={isSubscribePopoverOpen === id} toggle={() => setSubscribeValues(id, symbolTriggerData)} > <PopoverHeader> Notification Options <span className="popover-close"> <FontAwesomeIcon icon={faTimes} onClick={() => handlePopoverToggle(null)} /> </span> </PopoverHeader> {renderSubscribeOptions(id, isSubscribed, symbolTriggerData)} </Popover> </div> <Collapse isOpen={expandedStockId === id}> { isOpen(id) ? <StockTimeseries symbol={id}/> : null } </Collapse> </div>

Estamos usando el componente Card de ReactStrap para renderizar estas tarjetas. El componente Popover se utiliza para mostrar las opciones basadas en suscripción:

Opciones de notificación
Opciones de notificación. (Vista previa grande)

Cuando el usuario hace clic en el ícono de la bell para una acción en particular, puede optar por recibir una notificación cada hora o cuando el precio de la acción haya alcanzado el valor ingresado. Veremos esto en acción en la sección Eventos/Disparadores de tiempo.

Nota : ¡Llegaremos al componente StockTimeseries en la siguiente sección!

Consulte symbolList.js para obtener el código completo relacionado con el componente de lista de acciones.

Stock Timeseries Ver

El componente StockTimeseries utiliza la consulta stocksDataQuery :

 export const stocksDataQuery = gql` query getStocksData($symbol: String) { stock_data(order_by: {time: desc}, where: {symbol: {_eq: $symbol}}, limit: 25) { high low open close volume time } } `;

La consulta anterior obtiene los 25 puntos de datos recientes de la acción seleccionada. Por ejemplo, aquí está el gráfico de la métrica de apertura de acciones de Facebook:

Cronología de los precios de las acciones
Cronología de los precios de las acciones. (Vista previa grande)

Este es un componente sencillo en el que pasamos algunas opciones de gráfico al componente [ HighchartsReact ]. Estas son las opciones del gráfico:

 const chartOptions = { title: { text: `${symbol} Timeseries` }, subtitle: { text: 'Intraday (5min) open, high, low, close prices & volume' }, yAxis: { title: { text: '#' } }, xAxis: { title: { text: 'Time' }, categories: getDataPoints('time') }, legend: { layout: 'vertical', align: 'right', verticalAlign: 'middle' }, series: [ { name: 'high', data: getDataPoints('high') }, { name: 'low', data: getDataPoints('low') }, { name: 'open', data: getDataPoints('open') }, { name: 'close', data: getDataPoints('close') }, { name: 'volume', data: getDataPoints('volume') } ] }

El eje X muestra la hora y el eje Y muestra el valor de la métrica en ese momento. La función getDataPoints se utiliza para generar una serie de puntos para cada una de las series.

 const getDataPoints = (type) => { const values = []; data.stock_data.map((dataPoint) => { let value = dataPoint[type]; if (type === 'time') { value = new Date(dataPoint['time']).toLocaleString('en-US'); } values.push(value); }); return values; }

¡Sencillo! ¡Así es como se genera el componente Gráfico! Consulte los archivos Chart.js y stockTimeseries.js para obtener el código completo de las series temporales de stock.

Ahora debería estar listo con los datos y la parte de las interfaces de usuario del proyecto. Pasemos ahora a la parte interesante: configurar disparadores de evento/tiempo basados ​​en la entrada del usuario.

Configuración de activadores programados/de eventos

En esta sección, aprenderemos cómo configurar activadores en la consola de Hasura y cómo enviar notificaciones push web a los usuarios seleccionados. ¡Empecemos!

Disparadores de eventos en la consola Hasura

Vamos a crear un desencadenador de evento stock_value en la tabla stock_data e insert como operación desencadenante. El webhook se ejecutará cada vez que haya una inserción en la tabla stock_data .

Configuración de disparadores de eventos
Configuración de disparadores de eventos. (Vista previa grande)

Vamos a crear un proyecto de falla para la URL del webhook. Permítanme poner un poco sobre los webhooks para que sea fácil de entender:

Los webhooks se utilizan para enviar datos de una aplicación a otra cuando ocurre un evento en particular. Cuando se desencadena un evento, se realiza una llamada HTTP POST a la URL del webhook con los datos del evento como carga útil.

In this case, when there is an insert operation on the stock_data table, an HTTP post call will be made to the configured webhook URL (post call in the glitch project).

Glitch Project For Sending Web-push Notifications

We've to get the webhook URL to put in the above event trigger interface. Go to glitch.com and create a new project. In this project, we'll set up an express listener and there will be an HTTP post listener. The HTTP POST payload will have all the details of the stock datapoint including open , close , high , low , volume , time . We'll have to fetch the list of users subscribed to this stock with the value equal to the close metric.

These users will then be notified of the stock price via web-push notifications.

That's all we've to do to achieve the desired target of notifying users when the stock price reaches the expected value!

Let's break this down into smaller steps and implement them!

Installing Dependencies

We would need the following dependencies:

  • express : is used for creating an express server.
  • apollo-fetch : is used for creating a fetch function for getting data from the GraphQL endpoint.
  • web-push : is used for sending web push notifications.

Please write this script in package.json to run index.js on npm start command:

 "scripts": { "start": "node index.js" }

Setting Up Express Server

Let's create an index.js file as:

 const express = require('express'); const bodyParser = require('body-parser'); const app = express(); app.use(bodyParser.json()); const handleStockValueTrigger = (eventData, res) => { /* Code for handling this trigger */ } app.post('/', (req, res) => { const { body } = req const eventType = body.trigger.name const eventData = body.event switch (eventType) { case 'stock-value-trigger': return handleStockValueTrigger(eventData, res); } }); app.get('/', function (req, res) { res.send('Hello World - For Event Triggers, try a POST request?'); }); var server = app.listen(process.env.PORT, function () { console.log(`server listening on port ${process.env.PORT}`); });

In the above code, we've created post and get listeners on the route / . get is simple to get around! We're mainly interested in the post call. If the eventType is stock-value-trigger , we'll have to handle this trigger by notifying the subscribed users. Let's add that bit and complete this function!

Obtención de usuarios suscritos

 const fetch = createApolloFetch({ uri: process.env.GRAPHQL_URL }); const getSubscribedUsers = (symbol, triggerValue) => { return fetch({ query: `query getSubscribedUsers($symbol: String, $triggerValue: numeric) { events(where: {symbol: {_eq: $symbol}, trigger_type: {_eq: "event"}, trigger_value: {_gte: $triggerValue}}) { user_id user_subscription { subscription } } }`, variables: { symbol, triggerValue } }).then(response => response.data.events) } const handleStockValueTrigger = async (eventData, res) => { const symbol = eventData.data.new.symbol; const triggerValue = eventData.data.new.close; const subscribedUsers = await getSubscribedUsers(symbol, triggerValue); const webpushPayload = { title: `${symbol} - Stock Update`, body: `The price of this stock is ${triggerValue}` } subscribedUsers.map((data) => { sendWebpush(data.user_subscription.subscription, JSON.stringify(webpushPayload)); }) res.json(eventData.toString()); }

En la función handleStockValueTrigger anterior, primero buscamos los usuarios suscritos mediante la función getSubscribedUsers . Luego enviamos notificaciones push web a cada uno de estos usuarios. La función sendWebpush se utiliza para enviar la notificación. Veremos la implementación web-push en un momento.

La función getSubscribedUsers utiliza la consulta:

 query getSubscribedUsers($symbol: String, $triggerValue: numeric) { events(where: {symbol: {_eq: $symbol}, trigger_type: {_eq: "event"}, trigger_value: {_gte: $triggerValue}}) { user_id user_subscription { subscription } } }

Esta consulta toma el símbolo de cotización y el valor y obtiene los detalles del usuario, incluidos user-id y la user_subscription de usuario que coinciden con estas condiciones:

  • symbol igual al que se pasa en la carga útil.
  • trigger_type es igual a event .
  • trigger_value es mayor o igual que el que se pasa a esta función ( close en este caso).

Una vez que obtengamos la lista de usuarios, ¡lo único que queda es enviarles notificaciones push web! ¡Hagámoslo ahora mismo!

Envío de notificaciones web push a los usuarios suscritos

Primero tenemos que obtener las claves VAPID públicas y privadas para enviar notificaciones push web. Guarde estas claves en el archivo .env y configure estos detalles en index.js como:

 webPush.setVapidDetails( 'mailto:<YOUR_MAIL_ID>', process.env.PUBLIC_VAPID_KEY, process.env.PRIVATE_VAPID_KEY ); const sendWebpush = (subscription, webpushPayload) => { webPush.sendNotification(subscription, webpushPayload).catch(err => console.log('error while sending webpush', err)) }

La función sendNotification se usa para enviar web-push en el punto final de suscripción proporcionado como primer parámetro.

Eso es todo lo que se requiere para enviar con éxito notificaciones push web a los usuarios suscritos. Aquí está el código completo definido en index.js :

 const express = require('express'); const bodyParser = require('body-parser'); const { createApolloFetch } = require('apollo-fetch'); const webPush = require('web-push'); webPush.setVapidDetails( 'mailto:<YOUR_MAIL_ID>', process.env.PUBLIC_VAPID_KEY, process.env.PRIVATE_VAPID_KEY ); const app = express(); app.use(bodyParser.json()); const fetch = createApolloFetch({ uri: process.env.GRAPHQL_URL }); const getSubscribedUsers = (symbol, triggerValue) => { return fetch({ query: `query getSubscribedUsers($symbol: String, $triggerValue: numeric) { events(where: {symbol: {_eq: $symbol}, trigger_type: {_eq: "event"}, trigger_value: {_gte: $triggerValue}}) { user_id user_subscription { subscription } } }`, variables: { symbol, triggerValue } }).then(response => response.data.events) } const sendWebpush = (subscription, webpushPayload) => { webPush.sendNotification(subscription, webpushPayload).catch(err => console.log('error while sending webpush', err)) } const handleStockValueTrigger = async (eventData, res) => { const symbol = eventData.data.new.symbol; const triggerValue = eventData.data.new.close; const subscribedUsers = await getSubscribedUsers(symbol, triggerValue); const webpushPayload = { title: `${symbol} - Stock Update`, body: `The price of this stock is ${triggerValue}` } subscribedUsers.map((data) => { sendWebpush(data.user_subscription.subscription, JSON.stringify(webpushPayload)); }) res.json(eventData.toString()); } app.post('/', (req, res) => { const { body } = req const eventType = body.trigger.name const eventData = body.event switch (eventType) { case 'stock-value-trigger': return handleStockValueTrigger(eventData, res); } }); app.get('/', function (req, res) { res.send('Hello World - For Event Triggers, try a POST request?'); }); var server = app.listen(process.env.PORT, function () { console.log("server listening"); });

¡Probemos este flujo suscribiéndonos a acciones con algún valor e insertando manualmente ese valor en la tabla (para probar)!

Me suscribí a AMZN con un valor de 2000 y luego inserté un punto de datos en la tabla con este valor. Así es como la aplicación de notificación de acciones me notificó justo después de la inserción:

Insertar una fila en la tabla stock_data para probar
Insertar una fila en la tabla stock_data para probar. (Vista previa grande)

¡Limpio! También puede consultar el registro de invocación de eventos aquí:

Registro de eventos
Registro de eventos. (Vista previa grande)

¡El webhook está haciendo el trabajo como se esperaba! ¡Ya estamos listos para los activadores de eventos!

Disparadores programados/cron

Podemos lograr un disparador basado en el tiempo para notificar a los usuarios suscriptores cada hora usando el disparador de eventos Cron como:

Configuración de cronómetro/disparador programado
Configuración de cronómetro/disparador programado. (Vista previa grande)

Podemos usar la misma URL de webhook y manejar a los usuarios suscritos según el tipo de evento desencadenante como stock_price_time_based_trigger . La implementación es similar al disparador basado en eventos.

Conclusión

En este artículo, creamos una aplicación de notificación de precios de acciones. Aprendimos cómo buscar precios usando las API Alpha Vantage y almacenar los puntos de datos en la base de datos Postgres respaldada por Hasura. También aprendimos cómo configurar el motor Hasura GraphQL y crear disparadores programados y basados ​​en eventos. Creamos un proyecto de falla para enviar notificaciones push web a los usuarios suscritos.