Fermer

décembre 21, 2020

Création d'une application de notification des prix des actions à l'aide de React, Apollo GraphQL et Hasura


À propos de l'auteur

Ingénieur logiciel, essayant de donner un sens à chaque ligne de code qu'elle écrit. Ankita est une passionnée de JavaScript et adore ses parties étranges. Elle est aussi obsédée…
En savoir plus sur
Ankita
Masand

Dans cet article, nous allons apprendre à créer une application basée sur des événements et à envoyer une notification Web push lorsqu'un événement particulier est déclenché. Nous allons configurer des tables de base de données, des événements et des déclencheurs planifiés sur le moteur Hasura GraphQL et câbler le point de terminaison GraphQL à l'application frontale pour enregistrer la préférence de cours de l'action de l'utilisateur.

Le concept d'être notifié lorsque le l'événement de votre choix s'est produit est devenu populaire par rapport au fait d'être collé sur le flux continu de données pour trouver vous-même cet événement particulier. Les gens préfèrent recevoir des e-mails / messages pertinents lorsque leur événement préféré s'est produit plutôt que d'être accrochés à l'écran pour attendre que cet événement se produise. La terminologie basée sur les événements est également assez courante dans le monde des logiciels.

À quel point ce serait génial si vous pouviez obtenir les mises à jour du prix de votre action préférée sur votre téléphone?

Dans cet article, nous sommes va construire une application Stocks Price Notifier en utilisant les moteurs React, Apollo GraphQL et Hasura GraphQL. Nous allons démarrer le projet à partir d'un code standard create-react-app et tout reconstruire. Nous allons apprendre à configurer les tables de la base de données et les événements sur la console Hasura. Nous apprendrons également comment câbler les événements d'Hasura pour obtenir des mises à jour du cours des actions à l'aide de notifications Web push.

Voici un aperçu rapide de ce que nous allons construire:

 Aperçu de l'application de notification de cours de bourse
Cours de l'action Application Notifier

Allons-y!

Un aperçu de l'objet de ce projet

Les données sur les stocks (y compris des mesures telles que haut bas open close volume ) serait stocké dans une base de données Postgres soutenue par Hasura. L'utilisateur pourrait souscrire à un stock particulier en fonction d'une valeur ou il peut choisir d'être averti toutes les heures. L'utilisateur recevra une notification Web push une fois que ses critères d'abonnement seront remplis.

Cela ressemble à beaucoup de choses et il y aurait évidemment des questions ouvertes sur la façon dont nous allons construire ces éléments.

Voici un plan sur la façon dont nous accomplirions ce projet en quatre étapes:

  1. Récupération des données boursières à l'aide d'un script NodeJs
    Nous allons commencer par récupérer les données boursières à l'aide d'un simple script NodeJs auprès de l'un des fournisseurs d'API stocks – Alpha Vantage . Ce script récupérera les données pour un stock particulier à des intervalles de 5 minutes. La réponse de l'API inclut haut bas open close et volume . Ces données seront ensuite insérées dans la base de données Postgres qui est intégrée au back-end Hasura.
  2. Configuration du moteur Hasura GraphQL
    Nous allons ensuite mettre en place des tables sur la base de données Postgres pour enregistrer les points de données . Hasura génère automatiquement les schémas, requêtes et mutations GraphQL pour ces tables.
  3. Front-end utilisant React et Apollo Client
    L'étape suivante consiste à intégrer la couche GraphQL à l'aide du client Apollo et du fournisseur Apollo (le point de terminaison GraphQL fourni par Hasura). Les points de données seront affichés sous forme de graphiques sur le front-end. Nous allons également créer les options d'abonnement et déclencher les mutations correspondantes sur la couche GraphQL.
  4. Configuration des déclencheurs d'événements / programmés
    Hasura fournit un excellent outil autour des déclencheurs. Nous ajouterons des événements et des déclencheurs programmés dans le tableau de données sur les stocks. Ces déclencheurs seront définis si l'utilisateur souhaite recevoir une notification lorsque le cours des actions atteint une valeur particulière (déclencheur d'événement). L'utilisateur peut également choisir de recevoir une notification d'un stock particulier toutes les heures (déclenchement programmé).

Maintenant que le plan est prêt, mettons-le en action!

Voici le référentiel GitHub pour ce projet. Si vous vous perdez quelque part dans le code ci-dessous, référez-vous à ce référentiel et revenez à la vitesse supérieure!

Récupération des données boursières à l'aide d'un script NodeJs

Ce n'est pas si compliqué que cela puisse paraître! Nous devrons écrire une fonction qui récupère les données en utilisant le point de terminaison Alpha Vantage et cet appel de récupération doit être déclenché dans un intervalle de 5 minutes (Vous l'avez bien deviné, nous allons devez mettre cet appel de fonction dans setInterval ).

Si vous vous demandez toujours ce qu'est l'Alpha Vantage et que vous voulez juste sortir cela de votre tête avant de passer à la partie codage, alors le voici :

Alpha Vantage Inc. est un fournisseur leader d'API gratuites pour les données historiques et en temps réel sur les actions, le forex (FX) et les crypto-monnaies numériques.

Nous utiliserions ce point final pour obtenir les métriques requises d'un titre particulier. Cette API attend une clé API comme l'un des paramètres. Vous pouvez obtenir votre clé API gratuite à partir de ici . Nous sommes maintenant prêts à passer au bit intéressant – commençons à écrire du code!

Installation des dépendances

Créez un répertoire stocks-app et créez un répertoire server à l'intérieur il. Initialisez-le en tant que projet de nœud en utilisant npm init puis installez ces dépendances:

 npm i isomorphic-fetch pg nodemon --save 

Ce sont les trois seules dépendances que nous aurions besoin d'écrire ce script pour récupérer les cours des actions et les stocker dans la base de données Postgres.

Voici une brève explication de ces dépendances:

  • isomorphic-fetch
    Il est facile à utiliser fetch isomorphically ( sous la même forme) sur le client et sur le serveur.
  • pg
    Il s'agit d'un client PostgreSQL non bloquant pour NodeJs.
  • nodemon
    Il redémarre automatiquement le serveur sur tout changement de fichier dans le répertoire
Mise en place de la configuration

Ajoutez un fichier config.js au niveau racine. Ajoutez l'extrait de code ci-dessous dans ce fichier pour le moment:

 const config = {
  utilisateur: '',
  mot de passe: '',
  hôte: '',
  port: '',
  base de données: '',
  ssl: '',
  apiHost: 'https://www.alphavantage.co/',
};

module.exports = config; 

L'utilisateur password host port base de données ssl sont liés à la configuration Postgres. Nous reviendrons pour éditer ceci pendant que nous installons la partie moteur Hasura!

Initialisation du pool de connexions Postgres pour interroger la base de données

Un pool de connexions est un terme courant en informatique et vous J'entendrai souvent ce terme en traitant des bases de données.

Lors de l'interrogation des données dans les bases de données, vous devrez d'abord établir une connexion à la base de données. Cette connexion prend les informations d'identification de la base de données et vous donne un crochet pour interroger l'une des tables de la base de données.

Remarque : L'établissement de connexions à la base de données est coûteux et gaspille également des ressources importantes. Un pool de connexions met en cache les connexions de base de données et les réutilise sur les requêtes suivantes. Si toutes les connexions ouvertes sont utilisées, alors une nouvelle connexion est établie et est ensuite ajoutée au pool.

Maintenant que le pool de connexions est clair et à quoi il sert, commençons par créer une instance de le pool de connexions pg pour cette application:

Ajoutez le fichier pool.js au niveau racine et créez une instance de pool comme:

 const {Pool} = require (' pg ');
const config = require ('./ config');

const pool = nouveau pool ({
  utilisateur: config.user,
  mot de passe: config.password,
  hôte: config.host,
  port: config.port,
  base de données: config.database,
  ssl: config.ssl,
});

module.exports = pool; 

Les lignes de code ci-dessus créent une instance de Pool avec les options de configuration définies dans le fichier de configuration. Nous n'avons pas encore terminé le fichier de configuration, mais il n'y aura aucun changement lié aux options de configuration.

Nous avons maintenant défini le terrain et sommes prêts à commencer à faire des appels d'API au point de terminaison Alpha Vantage.

] Passons à la partie intéressante!

Récupération des données boursières

Dans cette section, nous allons récupérer les données boursières du point de terminaison Alpha Vantage. Voici le fichier index.js :

 const fetch = require ('isomorphic-fetch');
const getConfig = require ('./ config');
const {insertStocksData} = require ('./ queries');

symboles const = [
  'NFLX',
  'MSFT',
  'AMZN',
  'W',
  'FB'
];

(fonction getStocksData () {

  const apiConfig = getConfig ('apiHostOptions');
  const {hôte, timeSeriesFunction, intervalle, clé} = apiConfig;

  symboles.forEach ((symbole) => {
    fetch (`$ {host} query /? function = $ {timeSeriesFunction} & symbol = $ {symbol} & interval = $ {interval} & apikey = $ {key}`)
    .then ((res) => res.json ())
    .then ((données) => {
      const timeSeries = données ['Time Series (5min)'];
      Object.keys (timeSeries) .map ((clé) => {
        const dataPoint = timeSeries [key];
        charge utile const = [
          symbol,
          dataPoint['2. high'],
          dataPoint ['3. low'],
          dataPoint ['1. open'],
          dataPoint ['4. close'],
          dataPoint ['5. volume'],
          clé,
        ];
        insertStocksData (charge utile);
      });
    });
  })
}) () 

Pour les besoins de ce projet, nous allons interroger les prix uniquement pour ces actions – NFLX (Netflix), MSFT (Microsoft), AMZN (Amazon), W (Wayfair), FB (Facebook)

Reportez-vous à ce fichier pour les options de configuration. La fonction IIFE getStocksData ne fait pas grand-chose! Il parcourt ces symboles et interroge le point de terminaison Alpha Vantage $ {host} query /? Function = $ {timeSeriesFunction} & symbol = $ {symbol} & interval = $ {interval} & apikey = $ {key} pour obtenir les métriques pour ces stocks.

La fonction insertStocksData place ces points de données dans la base de données Postgres. Voici la fonction insertStocksData :

 const insertStocksData = async (payload) => {
  const query = 'INSERT INTO stock_data (symbole, haut, bas, ouvert, fermé, volume, heure) VALUES (1 $, 2 $, 3 $, 4 $, 5 $, 6 $, 7 $)';
  pool.query (requête, charge utile, (err, résultat) => {
    console.log ('résultat ici', err);
  });
}; 

Ça y est! Nous avons récupéré les points de données du stock de l'API Alpha Vantage et avons écrit une fonction pour les mettre dans la base de données Postgres dans la table stock_data . Il ne manque qu'une seule pièce pour que tout cela fonctionne! Nous devons renseigner les valeurs correctes dans le fichier de configuration. Nous obtiendrons ces valeurs après avoir configuré le moteur Hasura. Allons-y tout de suite!

Veuillez vous référer au répertoire server pour le code complet sur la récupération des points de données du point de terminaison Alpha Vantage et le remplissage dans la base de données Hasura Postgres.

Si cette approche de la configuration des connexions, les options de configuration et l'insertion de données à l'aide de la requête brute semblent un peu difficiles, ne vous inquiétez pas pour cela! Nous allons apprendre comment faire tout cela de manière simple avec une mutation GraphQL une fois le moteur Hasura mis en place!

Configuration du moteur Hasura GraphQL

Il est vraiment simple de configurer le moteur Hasura et d'obtenir opérationnel avec les schémas GraphQL, les requêtes, les mutations, les abonnements, les déclencheurs d'événements et bien plus encore!

Cliquez sur Try Hasura et entrez le nom du projet:

 Creating a Hasura Project
Créer un projet Hasura. ( Grand aperçu )

J'utilise la base de données Postgres hébergée sur Heroku. Créez une base de données sur Heroku et liez-la à ce projet. Vous devriez alors être prêt à profiter de la puissance de la console Hasura riche en requêtes.

Veuillez copier l'URL de la base de données Postgres que vous obtiendrez après la création du projet. Nous devrons le mettre dans le fichier de configuration.

Cliquez sur Launch Console et vous serez redirigé vers cette vue:

 Hasura Console
Hasura Console. ( Grand aperçu )

Commençons par construire le schéma de table dont nous aurions besoin pour ce projet.

Création d'un schéma de tables sur la base de données Postgres

Veuillez aller dans l'onglet Données et cliquer sur Ajouter une table! Commençons par créer quelques-unes des tables:

symbole table

Cette table serait utilisée pour stocker les informations des symboles. Pour l’instant, j’ai gardé deux champs ici: id et entreprise . Le champ id est une clé primaire et company est de type varchar . Ajoutons quelques-uns des symboles de ce tableau:

 table des symboles
table des symboles . ( Grand aperçu )
table stock_data

La table stock_data table stores id symbole time et les paramètres tels que high low open close volume . Le script NodeJs que nous avons écrit plus tôt dans cette section sera utilisé pour remplir cette table particulière.

Voici à quoi ressemble la table:

 stock_data table
stock_data table. ( Grand aperçu )

Super! Passons à l'autre table du schéma de base de données! Table

user_subscription

La table user_subscription stocke l'objet d'abonnement par rapport à l'ID utilisateur. Cet objet d'abonnement est utilisé pour envoyer des notifications push web aux utilisateurs. Nous apprendrons plus tard dans l'article comment générer cet objet d'abonnement.

Il y a deux champs dans ce tableau – id est la clé primaire de type uuid et le champ d'abonnement est de type jsonb .

events table

Ceci est le plus important et est utilisé pour stocker les options d'événement de notification. Lorsqu'un utilisateur opte pour les mises à jour de prix d'un stock particulier, nous stockons ces informations d'événement dans ce tableau. Cette table contient les colonnes suivantes:

  • id : est une clé primaire avec la propriété d'auto-incrémentation.
  • symbole : est un champ de texte.
  • user_id : est de type uuid .
  • trigger_type : est utilisé pour stocker le type de déclencheur d'événement – heure / événement .
  • trigger_value : est utilisé pour stocker la valeur de déclenchement. Par exemple, si un utilisateur a opté pour le déclencheur d'événement basé sur le prix – il souhaite des mises à jour si le prix de l'action a atteint 1000, alors trigger_value serait 1000 et le trigger_type serait event .

Ce sont toutes les tables dont nous aurions besoin pour ce projet. Nous devons également établir des relations entre ces tables pour avoir un flux de données et des connexions fluides.

Configuration des relations entre les tables

La table events est utilisée pour envoyer des notifications push Web basées sur la valeur de l'événement. Il est donc judicieux de connecter cette table à la table user_subscription pour pouvoir envoyer des notifications push sur les abonnements stockés dans cette table.

 events.user_id → user_subscription.id 

The The ] La table stock_data est liée à la table des symboles comme suit:

 stock_data.symbol → symbol.id 

Nous devons également construire des relations sur la table symbol comme:

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

Nous avons maintenant créé les tables requises et également établi les relations entre elles! Passons à l'onglet GRAPHIQL de la console pour voir la magie!

Hasura a déjà configuré les requêtes GraphQL basées sur ces tables:

 Requêtes / mutations GraphQL sur la console Hasura
Requêtes / mutations GraphQL sur la console Hasura. ( Grand aperçu )

Il est très simple d'interroger ces tables et vous pouvez également appliquer l'un de ces filtres / propriétés ( distinct_on limit offset order_by where ) pour obtenir les données souhaitées.

Tout cela semble bon, mais nous n'avons toujours pas connecté notre code côté serveur au Console Hasura.

Connexion du script NodeJs à la base de données Postgres

Veuillez mettre les options requises dans le fichier config.js du répertoire server comme:

 ] const config = {
  databaseOptions: {
    utilisateur: '',
    mot de passe: '',
    hôte: '',
    port: '',
    base de données: '',
    ssl: vrai,
  },
  apiHostOptions: {
    hôte: 'https://www.alphavantage.co/',
    clé: '',
    timeSeriesFunction: 'TIME_SERIES_INTRADAY',
    intervalle: '5min'
  },
  graphqlURL: ""
};

const getConfig = (clé) => {
  return config [key];
};

module.exports = getConfig; 

Veuillez mettre ces options à partir de la chaîne de base de données qui a été générée lorsque nous avons créé la base de données Postgres sur Heroku.

Le apiHostOptions comprend les options liées à l'API telles que host key timeSeriesFunction and interval .

Vous obtiendrez le champ graphqlURL dans le ] Onglet GRAPHIQL de la console Hasura.

La fonction getConfig est utilisée pour renvoyer la valeur demandée à partir de l'objet de configuration. Nous l'avons déjà utilisé dans index.js dans le répertoire server .

Il est temps d'exécuter le serveur et de renseigner certaines données dans la base de données. J'ai ajouté un script dans package.json en tant que:

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

Exécutez npm start sur le terminal et les points de données du tableau de symboles dans index.js doivent être renseignés dans les tables.

Refactorisation de la requête brute dans le NodeJs Script To GraphQL Mutation

Maintenant que le moteur Hasura est configuré, voyons comme il est facile d'appeler une mutation sur la table stock_data .

La fonction insertStocksData ] in queries.js utilise une requête brute:

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

Refactorisons cette requête et utilisons une mutation alimentée par le moteur Hasura. Voici le queries.js remanié dans le répertoire du serveur:


const {createApolloFetch} = require ('apollo-fetch');
const getConfig = require ('./ config');

const GRAPHQL_URL = getConfig ('graphqlURL');
const fetch = createApolloFetch ({
  uri: GRAPHQL_URL,
});

const insertStocksData = async (charge utile) => {
  const insertStockMutation = attendre la récupération ({
    query: `mutation insertStockData ($ objects: [stock_data_insert_input!]!) {
      insert_stock_data (objets: $ objets) {
        retour {
          id
        }
      }
    } `,
    variables: {
      objets: charge utile,
    },
  });
  console.log ('insertStockMutation', insertStockMutation);
};

module.exports = {
  insertStocksData
} 

Remarque: Nous devons ajouter graphqlURL dans le fichier config.js .

Le fichier apollo-fetch ] Le module renvoie une fonction d'extraction qui peut être utilisée pour interroger / muter la date sur le point de terminaison GraphQL. Assez simple, non?

Le seul changement que nous devons faire dans index.js est de renvoyer l'objet stocks dans le format requis par la fonction insertStocksData . Veuillez consulter index2.js et queries2.js pour le code complet avec cette approche.

Maintenant que nous avons terminé le côté données du projet, passons à le bit frontal et construisez des composants intéressants!

Note : Nous n'avons pas à conserver les options de configuration de la base de données avec cette approche!

Front-end utilisant React And Apollo Client

Le projet frontal se trouve dans le même référentiel et est créé à l'aide du package create-react-app . Le technicien de service généré à l'aide de ce package prend en charge la mise en cache des actifs, mais il ne permet pas d'ajouter plus de personnalisations au fichier du service worker. Il y a déjà quelques problèmes ouverts pour ajouter la prise en charge des options de service worker personnalisé. Il existe des moyens de résoudre ce problème et d’ajouter la prise en charge d’un service worker personnalisé.

Commençons par examiner la structure du projet frontal:

 Project Directory
Project Directory. ( Grand aperçu )

Veuillez vérifier le répertoire src ! Ne vous inquiétez pas pour le moment des fichiers liés au service worker. Nous en apprendrons plus sur ces fichiers plus loin dans cette section. Le reste de la structure du projet semble simple. Le dossier components aura les composants (Loader, Chart); le dossier services contient certaines des fonctions / services d'assistance utilisés pour transformer des objets dans la structure requise; styles comme son nom l'indique contient les fichiers sass utilisés pour styliser le projet; views est le répertoire principal et il contient les composants de la couche de vue.

Nous n'aurions besoin que de deux composants de vue pour ce projet: la liste des symboles et la série temporelle des symboles. Nous allons créer la série chronologique à l'aide du composant Chart de la bibliothèque highcharts. Commençons par ajouter du code dans ces fichiers pour construire les pièces sur le front-end!

Installation des dépendances

Voici la liste des dépendances dont nous aurons besoin:

Toutes ces dépendances auront un sens une fois que nous commencerons à les utiliser dans le projet.

Configuration du client Apollo

Créez un apolloClient.js dans le dossier src comme:

 import ApolloClient depuis 'apollo-boost ';

const apolloClient = new ApolloClient ({
  uri: ""
});

export default apolloClient; 

Le code ci-dessus instancie ApolloClient et prend uri dans les options de configuration. Le uri est l'URL de votre console Hasura. Vous obtiendrez ce champ uri dans l'onglet GRAPHIQL dans la section GraphQL Endpoint .

Le code ci-dessus semble simple mais il prend en charge le principal une partie du projet! Il connecte le schéma GraphQL construit sur Hasura avec le projet actuel.

Nous devons également passer cet objet client apollo à ApolloProvider et envelopper le composant racine dans ApolloProvider . Cela permettra à tous les composants imbriqués dans le composant principal d'utiliser client prop et de lancer des requêtes sur cet objet client.

Modifions le fichier index.js comme suit:

 const Wrapper = () => {
/ * une logique de service worker - ignorer pour l'instant * /
  const [insertSubscription] = useMutation (subscriptionMutation);
  useEffect (() => {
    serviceWorker.register (insertSubscription);
  }, [])
  / * ignorer l'extrait de code ci-dessus * /
  retour ;
}

ReactDOM.render (
  
    
  ,
  document.getElementById ('racine')
); 

Veuillez ignorer le code associé à insertSubscription . Nous comprendrons cela en détail plus tard. Le reste du code doit être simple à déplacer. La fonction render prend le composant racine et l'élémentId comme paramètres. Remarquez que le client (instance ApolloClient) est passé comme accessoire à ApolloProvider . Vous pouvez consulter le fichier complet index.js ici .

Configuration du Service Worker personnalisé

Un Service worker est un fichier JavaScript capable d'intercepter les requêtes réseau . Il est utilisé pour interroger le cache pour vérifier si l'actif demandé est déjà présent dans le cache au lieu de faire un trajet vers le serveur. Les techniciens de service sont également utilisés pour envoyer des notifications Web push aux appareils abonnés.

Nous devons envoyer des notifications Web push pour les mises à jour du cours de l'action aux utilisateurs abonnés. Définissons le terrain et construisons ce fichier de service worker!

Le lien insertSubscription extrait dans le fichier index.js effectue le travail d'enregistrement du service worker et de placement de l'objet d'abonnement dans la base de données utilisant subscriptionMutation .

Veuillez consulter queries.js pour toutes les requêtes et mutations utilisées dans le projet.

serviceWorker.register (insertSubscription); invoque la fonction register écrite dans le fichier serviceWorker.js . Le voici:

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

La fonction ci-dessus vérifie d'abord si serviceWorker est pris en charge par le navigateur, puis enregistre le fichier de service worker hébergé sur l'URL swUrl . Nous vérifierons ce fichier dans un instant!

La fonction getSubscription effectue le travail d'obtention de l'objet d'abonnement en utilisant la méthode subscribe sur l'objet pushManager . Cet objet d'abonnement est ensuite stocké dans la table user_subscription par rapport à un userId. Veuillez noter que l'ID utilisateur est généré à l'aide de la fonction uuid . Jetons un œil à la fonction getSubscription :

 const getSubscription = (serviceWorkerRegistration, insertSubscription) => {
  serviceWorkerRegistration.pushManager.getSubscription ()
    .then ((abonnement) => {
      const userId = uuidv4 ();
      if (! abonnement) {
        const applicationServerKey = urlB64ToUint8Array ('')
        serviceWorkerRegistration.pushManager.subscribe ({
          userVisibleOnly: true,
          applicationServerKey
        }). puis (abonnement => {
          insertSubscription ({
            variables: {
              identifiant d'utilisateur,
              abonnement
            }
          });
          localStorage.setItem ('serviceWorkerRegistration', JSON.stringify ({
            identifiant d'utilisateur,
            abonnement
          }));
        })
      }
    })
} 

Vous pouvez consulter le fichier serviceWorker.js pour le code complet!

 Notification Popup
Notification Popup. ( Grand aperçu )

Notification.requestPermission () a invoqué ce popup qui demande à l'utilisateur l'autorisation d'envoyer des notifications. Une fois que l'utilisateur clique sur Autoriser, un objet d'abonnement est généré par le service push. Nous stockons cet objet dans le localStorage en tant que:

 Objet Webpush Subscriptions
Objet Webpush Subscriptions. ( Grand aperçu )

Le champ endpoint dans l'objet ci-dessus est utilisé pour identifier le périphérique et le serveur utilise ce point de terminaison pour envoyer des notifications push Web à l'utilisateur.

Nous avons effectué le travail d'initialisation et d'enregistrement du technicien de service. Nous avons également l'objet d'abonnement de l'utilisateur! Cela fonctionne très bien grâce au fichier serviceWorker.js présent dans le dossier public . Configurons maintenant le technicien de service pour qu'il prépare les choses!

C'est un sujet un peu difficile, mais allons-y! Comme mentionné précédemment, l'utilitaire create-react-app ne prend pas en charge les personnalisations par défaut pour le technicien de service. Nous pouvons réaliser l'implémentation du service client en utilisant le module workbox-build .

Nous devons également nous assurer que le comportement par défaut des fichiers de pré-cache est intact. Nous allons modifier la partie dans laquelle le technicien de service est intégré dans le projet. Et, workbox-build aide à atteindre exactement cela! Des trucs sympas! Restons simples et listons tout ce que nous avons à faire pour faire fonctionner le service worker personnalisé:

  • Gérez la pré-mise en cache des actifs à l'aide de workboxBuild .
  • Créez un modèle de service worker pour la mise en cache assets.
  • Créez le fichier sw-precache-config.js pour fournir des options de configuration personnalisées.
  • Ajoutez le script de construction du service worker à l'étape de construction dans package.json .

Ne vous inquiétez pas si tout cela vous semble déroutant! L'article ne se concentre pas sur l'explication de la sémantique derrière chacun de ces points. Nous devons nous concentrer sur la mise en œuvre pour le moment! J'essaierai de couvrir le raisonnement derrière tout le travail pour créer un service worker personnalisé dans un autre article.

Créons deux fichiers sw-build.js et sw-custom.js dans le répertoire src . Veuillez vous référer aux liens vers ces fichiers et ajouter le code à votre projet.

Créons maintenant le fichier sw-precache-config.js au niveau racine et ajoutons le code suivant dans ce fichier: [19659046] module.exports = {
staticFileGlobs: [
'build/static/css/**.css',
'build/static/js/**.js',
'build/index.html'
],
swFilePath: './build/serviceWorker.js',
stripPrefix: 'build /',
handleFetch: faux,
runtimeCaching: [{
urlPattern: /this\.is\.a\.regex/,
handler: 'networkFirst'
}]
}

Let’s also modify the package.json file to make room for building the custom service worker file:

Add these statements in the scripts section:

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

And modify the build script as:

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

The setup is finally done! We now have to add a custom service worker file inside the public folder:

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

We’ve just added one push listener to listen to push-notifications being sent by the server. The function showNotification is used for displaying web push notifications to the user.

This is it! We’re done with all the hard work of setting up a custom service worker to handle web push notifications. We’ll see these notifications in action once we build the user interfaces!

We’re getting closer to building the main code pieces. Let’s now start with the first view!

Symbol List View

The App component being used in the previous section looks like this:

import React from 'react';
import SymbolList from './views/symbolList';

const App = () => {
  return ;
};

export default App;

It is a simple component that returns SymbolList view and SymbolList does all the heavy-lifting of displaying symbols in a neatly tied user interface.

Let’s look at symbolList.js inside the views folder:

Please refer to the file here!

The component returns the results of the renderSymbols function. And, this data is being fetched from the database using the useQuery hook as:

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

The symbolsQuery is defined as:

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 {
            haute
            volume
          }
          min {
            faible
            volume
          }
        }
      }
    }
  }
`;

It takes in userId and fetches the subscribed events of that particular user to display the correct state of the notification icon (bell icon that is being displayed along with the title). The query also fetches the max and min values of the stock. Notice the use of aggregate in the above query. Hasura’s Aggregation queries do the work behind the scenes to fetch the aggregate values like countsumavgmaxminetc.

Based on the response from the above GraphQL call, here’s the list of cards that are displayed on the front-end:

Stock Cards
Stock Cards. (Large preview)

The card HTML structure looks something like this:

{company} {id}
High: {max.high} {' '}(Volume: {max.volume})
Low: {min.low} {' '}(Volume: {min.volume})
{' '}
setSubscribeValues(id, symbolTriggerData)} > Notification Options handlePopoverToggle(null)} /> {renderSubscribeOptions(id, isSubscribed, symbolTriggerData)}
{ isOpen(id) ? : null }

We’re using the Card component of ReactStrap to render these cards. The Popover component is used for displaying the subscription-based options:

Notification Options
Notification Options. (Large preview)

When the user clicks on the bell icon for a particular stock, he can opt-in to get notified every hour or when the price of the stock has reached the entered value. We’ll see this in action in the Events/Time Triggers section.

Note: We’ll get to the StockTimeseries component in the next section!

Please refer to symbolList.js for the complete code related to the stocks list component.

Stock Timeseries View

The StockTimeseries component uses the query stocksDataQuery:

export const stocksDataQuery = gql`
  query getStocksData($symbol: String) {
    stock_data(order_by: {time: desc}, where: {symbol: {_eq: $symbol}}, limit: 25) {
      haute
      faible
      ouvert
      Fermer
      volume
      temps
    }
  }
`;

The above query fetches the recent 25 data points of the selected stock. For example, here is the chart for the Facebook stock open metric:

Stock Prices timeline
Stock Prices timeline. (Large preview)

This is a straightforward component where we pass in some chart options to [HighchartsReact] component. Here are the chart options:

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')
    }
  ]
}

The X-Axis shows the time and the Y-Axis shows the metric value at that time. The function getDataPoints is used for generating a series of points for each of the 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;
}

Simple! That’s how the Chart component is generated! Please refer to Chart.js and stockTimeseries.js files for the complete code on stock time-series.

You should now be ready with the data and the user interfaces part of the project. Let’s now move onto the interesting part — setting up event/time triggers based on the user’s input.

Setting Up Event/Scheduled Triggers

In this section, we’ll learn how to set up triggers on the Hasura console and how to send web push notifications to the selected users. Let’s get started!

Events Triggers On Hasura Console

Let’s create an event trigger stock_value on the table stock_data and insert as the trigger operation. The webhook will run every time there is an insert in the stock_data table.

Event triggers setup
Event triggers setup. (Large preview)

We’re going to create a glitch project for the webhook URL. Let me put down a bit about webhooks to make easy clear to understand:

Webhooks are used for sending data from one application to another on the occurrence of a particular event. When an event is triggered, an HTTP POST call is made to the webhook URL with the event data as the payload.

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 openclosehighlowvolumetime. 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-triggerwe’ll have to handle this trigger by notifying the subscribed users. Let’s add that bit and complete this function!

Fetching Subscribed Users
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());
}

In the above handleStockValueTrigger function, we’re first fetching the subscribed users using the getSubscribedUsers function. We’re then sending web-push notifications to each of these users. The function sendWebpush is used for sending the notification. We’ll look at the web-push implementation in a moment.

The function getSubscribedUsers uses the 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
    }
  }
}

This query takes in the stock symbol and the value and fetches the user details including user-id and user_subscription that matches these conditions:

  • symbol equal to the one being passed in the payload.
  • trigger_type is equal to event.
  • trigger_value is greater than or equal to the one being passed to this function (close in this case).

Once we get the list of users, the only thing that remains is sending web-push notifications to them! Let’s do that right away!

Sending Web-Push Notifications To The Subscribed Users

We’ve to first get the public and the private VAPID keys to send web-push notifications. Please store these keys in the .env file and set these details in index.js as:

webPush.setVapidDetails(
  'mailto:',
  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))
}

The sendNotification function is used for sending the web-push on the subscription endpoint provided as the first parameter.

That’s all is required to successfully send web-push notifications to the subscribed users. Here’s the complete code defined in index.js:

const express = require('express');
const bodyParser = require('body-parser');
const { createApolloFetch } = require('apollo-fetch');
const webPush = require('web-push');

webPush.setVapidDetails(
  'mailto:',
  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");
});

Let’s test out this flow by subscribing to stock with some value and manually inserting that value in the table (for testing)!

I subscribed to AMZN with value as 2000 and then inserted a data point in the table with this value. Here’s how the stocks notifier app notified me right after the insertion:

Inserting a row in stock_data table for testing
Inserting a row in stock_data table for testing. (Large preview)

Neat! You can also check the event invocation log here:

Event Log
Event Log. (Large preview)

The webhook is doing the work as expected! We’re all set for the event triggers now!

Scheduled/Cron Triggers

We can achieve a time-based trigger for notifying the subscriber users every hour using the Cron event trigger as:

Cron/Scheduled Trigger setup
Cron/Scheduled Trigger setup. (Large preview)

We can use the same webhook URL and handle the subscribed users based on the trigger event type as stock_price_time_based_trigger. The implementation is similar to the event-based trigger.

Conclusion

In this article, we built a stock price notifier application. We learned how to fetch prices using the Alpha Vantage APIs and store the data points in the Hasura backed Postgres database. We also learned how to set up the Hasura GraphQL engine and create event-based and scheduled triggers. We built a glitch project for sending web-push notifications to the subscribed users.

Smashing Editorial(ra, yk, il)




Source link