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

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:

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:
- 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. - 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. - 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. - 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 à utiliserfetch
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:

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:

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
. ( 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. ( 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 typeuuid
.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, alorstrigger_value
serait 1000 et letrigger_type
seraitevent
.
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:

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:

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:
apollo-boost
Apollo boost est un méthode zero-config pour commencer à utiliser Apollo Client. Il est livré avec les options de configuration par défaut.reactstrap
etbootstrap
Les composants sont construits à l'aide de ces deux packages.graphql
andgraphql-type-json [19659039]
graphql
est une dépendance requise pour utiliserapollo-boost
etgraphql-type-json
est utilisé pour supporter le type de donnéesjson
étant utilisé dans le schéma GraphQL.highcharts
andhighcharts-react-official
Et ces deux packages seront utilisés pour construire le graphique:node-sass
Ceci est ajouté pour supporte les fichiers sass pour le style.uuid
Ce paquet est utilisé pour générer des valeurs aléatoires fortes.
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.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:

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 count
sum
avg
max
min
etc.
Based on the response from the above GraphQL call, here’s the list of cards that are displayed on the front-end:

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:

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:

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.

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 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!
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 toevent
.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:

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

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:

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.

Source link