Construindo um aplicativo de notificação de preços de ações usando React, Apollo GraphQL e Hasura
Publicados: 2022-03-10O conceito de ser notificado quando o evento de sua escolha ocorreu tornou-se popular em comparação com ser colado no fluxo contínuo de dados para encontrar essa ocorrência específica por conta própria. As pessoas preferem receber e-mails/mensagens relevantes quando o evento de sua preferência ocorreu, em vez de ficarem vidrados na tela para esperar que esse evento aconteça. A terminologia baseada em eventos também é bastante comum no mundo do software.
Quão incrível seria se você pudesse obter as atualizações do preço de suas ações favoritas em seu telefone?
Neste artigo, vamos construir um aplicativo Stocks Price Notifier usando o mecanismo React, Apollo GraphQL e Hasura GraphQL. Vamos iniciar o projeto a partir de um código clichê create-react-app
e construiremos tudo do zero. Aprenderemos como configurar as tabelas de banco de dados e eventos no console Hasura. Também aprenderemos como conectar os eventos da Hasura para obter atualizações de preços de ações usando notificações push da web.
Aqui está uma rápida olhada no que estaríamos construindo:

Vamos indo!
Uma visão geral do que é este projeto
Os dados de ações (incluindo métricas como high , low , open , close , volume ) seriam armazenados em um banco de dados Postgres apoiado pelo Hasura. O usuário poderia assinar uma determinada ação com base em algum valor ou pode optar por ser notificado a cada hora. O usuário receberá uma notificação web-push assim que seus critérios de assinatura forem atendidos.
Isso parece um monte de coisas e obviamente haveria algumas questões em aberto sobre como construiremos essas peças.
Aqui está um plano de como realizaríamos este projeto em quatro etapas:
- Buscando os dados de ações usando um script NodeJs
Começaremos buscando os dados de ações usando um script NodeJs simples de um dos provedores de API de ações — Alpha Vantage. Este script buscará os dados de uma determinada ação em intervalos de 5 minutos. A resposta da API inclui alto , baixo , aberto , fechado e volume . Esses dados serão então inseridos no banco de dados Postgres que está integrado ao back-end Hasura. - Configurando o mecanismo Hasura GraphQL
Em seguida, configuraremos algumas tabelas no banco de dados Postgres para registrar os pontos de dados. O Hasura gera automaticamente os esquemas, consultas e mutações do GraphQL para essas tabelas. - Front-end usando React e Apollo Client
A próxima etapa é integrar a camada GraphQL usando o cliente Apollo e o provedor Apollo (o endpoint GraphQL fornecido pela Hasura). Os pontos de dados serão mostrados como gráficos no front-end. Também criaremos as opções de assinatura e dispararemos as mutações correspondentes na camada GraphQL. - Configurando acionadores de evento/agendados
Hasura fornece uma ferramenta excelente em torno de gatilhos. Adicionaremos acionadores de eventos e agendados na tabela de dados de ações. Esses gatilhos serão definidos se o usuário estiver interessado em receber uma notificação quando os preços das ações atingirem um determinado valor (gatilho de evento). O usuário também pode optar por receber uma notificação de um determinado estoque a cada hora (gatilho programado).
Agora que o plano está pronto, vamos colocá-lo em ação!
Aqui está o repositório GitHub para este projeto. Se você se perder em algum lugar no código abaixo, consulte este repositório e volte à velocidade!
Buscando os dados de ações usando um script NodeJs
Isso não é tão complicado quanto parece! Teremos que escrever uma função que busca dados usando o endpoint Alpha Vantage e esta chamada de busca deve ser disparada em um intervalo de 5 minutos (Você acertou, teremos que colocar esta chamada de função em setInterval
).
Se você ainda está se perguntando o que é Alpha Vantage e só quer tirar isso da cabeça antes de pular para a parte de codificação, aqui está:
A Alpha Vantage Inc. é uma fornecedora líder de APIs gratuitas para dados históricos e em tempo real sobre ações, forex (FX) e digitais/criptomoedas.
Estaríamos usando esse endpoint para obter as métricas necessárias de uma determinada ação. Esta API espera uma chave de API como um dos parâmetros. Você pode obter sua chave de API gratuita aqui. Agora estamos prontos para entrar na parte interessante — vamos começar a escrever algum código!
Instalando dependências
Crie um diretório stocks-app
e crie um diretório de server
dentro dele. Inicialize-o como um projeto de nó usando npm init
e instale estas dependências:
npm i isomorphic-fetch pg nodemon --save
Estas são as únicas três dependências que precisaríamos para escrever este script para buscar os preços das ações e armazená-los no banco de dados Postgres.
Aqui está uma breve explicação dessas dependências:
-
isomorphic-fetch
Isso facilita o uso dafetch
isomorficamente (na mesma forma) no cliente e no servidor. -
pg
É um cliente PostgreSQL sem bloqueio para NodeJs. -
nodemon
Ele reinicia automaticamente o servidor em qualquer alteração de arquivo no diretório.
Configurando a configuração
Adicione um arquivo config.js
no nível raiz. Adicione o trecho de código abaixo nesse arquivo por enquanto:
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;
O user
, password
, host
, port
, database
de dados , ssl
estão relacionados à configuração do Postgres. Voltaremos para editar isso enquanto configuramos a parte do motor Hasura!
Inicializando o pool de conexões do Postgres para consultar o banco de dados
Um connection pool
é um termo comum em ciência da computação e você ouvirá esse termo com frequência ao lidar com bancos de dados.
Ao consultar dados em bancos de dados, você terá que primeiro estabelecer uma conexão com o banco de dados. Essa conexão recebe as credenciais do banco de dados e fornece um gancho para consultar qualquer uma das tabelas no banco de dados.
Nota : Estabelecer conexões de banco de dados é caro e também desperdiça recursos significativos. Um pool de conexões armazena em cache as conexões de banco de dados e as reutiliza em consultas sucessivas. Se todas as conexões abertas estiverem em uso, uma nova conexão será estabelecida e adicionada ao pool.
Agora que está claro o que é o pool de conexões e para que ele é usado, vamos começar criando uma instância do pool de conexões pg
para este aplicativo:
Adicione o arquivo pool.js
no nível raiz e crie uma instância de pool 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;
As linhas de código acima criam uma instância de Pool
com as opções de configuração definidas no arquivo de configuração. Ainda estamos para concluir o arquivo de configuração, mas não haverá alterações relacionadas às opções de configuração.
Nós já preparamos o terreno e estamos prontos para começar a fazer algumas chamadas de API para o endpoint Alpha Vantage.
Vamos para a parte interessante!
Buscando os dados de ações
Nesta seção, buscaremos os dados de estoque do endpoint Alpha Vantage. Aqui está o arquivo 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); }); }); }) })()
Para o propósito deste projeto, vamos consultar os preços apenas para essas ações — NFLX (Netflix), MSFT (Microsoft), AMZN (Amazon), W (Wayfair), FB (Facebook).
Consulte este arquivo para as opções de configuração. A função IIFE getStocksData
não está fazendo muito! Ele percorre esses símbolos e consulta o endpoint Alpha Vantage ${host}query/?function=${timeSeriesFunction}&symbol=${symbol}&interval=${interval}&apikey=${key}
para obter as métricas para essas ações.
A função insertStocksData
coloca esses pontos de dados no banco de dados Postgres. Aqui está a função 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); }); };
É isso! Buscamos pontos de dados do estoque da API Alpha Vantage e escrevemos uma função para colocá-los no banco de dados Postgres na tabela stock_data
. Falta apenas uma peça para fazer tudo isso funcionar! Temos que preencher os valores corretos no arquivo de configuração. Obteremos esses valores depois de configurar o mecanismo Hasura. Vamos a isso imediatamente!
Consulte o diretório do server
para obter o código completo sobre como buscar pontos de dados do terminal Alpha Vantage e preenchê-los no banco de dados Hasura Postgres.
Se essa abordagem de configurar conexões, opções de configuração e inserir dados usando a consulta bruta parecer um pouco difícil, não se preocupe com isso! Vamos aprender como fazer tudo isso de maneira fácil com uma mutação GraphQL assim que o mecanismo Hasura estiver configurado!
Configurando o mecanismo Hasura GraphQL
É muito simples configurar o mecanismo Hasura e colocar em funcionamento os esquemas, consultas, mutações, assinaturas, gatilhos de eventos do GraphQL e muito mais!
Clique em Try Hasura e digite o nome do projeto:

Estou usando o banco de dados Postgres hospedado no Heroku. Crie um banco de dados no Heroku e vincule-o a este projeto. Você deve estar pronto para experimentar o poder do console Hasura rico em consultas.
Por favor, copie a URL do Postgres DB que você obterá após criar o projeto. Teremos que colocar isso no arquivo de configuração.
Clique em Launch Console e você será redirecionado para esta visualização:

Vamos começar a construir o esquema de tabela que precisaríamos para este projeto.
Criando o esquema de tabelas no banco de dados Postgres
Por favor, vá para a guia Dados e clique em Adicionar Tabela! Vamos começar a criar algumas das tabelas:
tabela de symbol
Esta tabela seria usada para armazenar as informações dos símbolos. Por enquanto, mantive dois campos aqui — id
e company
. O id
do campo é uma chave primária e a company
é do tipo varchar
. Vamos adicionar alguns dos símbolos nesta tabela:

symbol
. (Visualização grande) tabela stock_data
A tabela stock_data
armazena id
, symbol
, time
e as métricas como high
, low
, open
, close
, volume
. O script NodeJs que escrevemos anteriormente nesta seção será usado para preencher essa tabela específica.
Veja como fica a tabela:

stock_data
. (Visualização grande)Arrumado! Vamos para a outra tabela no esquema do banco de dados!
tabela user_subscription
A tabela user_subscription
armazena o objeto de assinatura em relação ao ID do usuário. Este objeto de assinatura é usado para enviar notificações push da Web para os usuários. Aprenderemos mais adiante neste artigo como gerar esse objeto de assinatura.
Existem dois campos nesta tabela — id
é a chave primária do tipo uuid
e o campo de assinatura é do tipo jsonb
.
tabela de events
Este é o mais importante e é usado para armazenar as opções de eventos de notificação. Quando um usuário opta pelas atualizações de preço de uma determinada ação, armazenamos as informações desse evento nesta tabela. Esta tabela contém estas colunas:
-
id
: é uma chave primária com a propriedade de incremento automático. -
symbol
: é um campo de texto. -
user_id
: é do tipouuid
. -
trigger_type
: é usado para armazenar o tipo de trigger de evento —time/event
. -
trigger_value
: é usado para armazenar o valor do gatilho. Por exemplo, se um usuário optou pelo acionador de evento baseado em preço — ele deseja atualizações se o preço da ação atingiu 1.000, então otrigger_value
seria 1.000 e otrigger_type
seriaevent
.
Estas são todas as tabelas que precisamos para este projeto. Também temos que configurar relações entre essas tabelas para ter um fluxo de dados e conexões suaves. Vamos fazer isso!
Configurando relações entre tabelas
A tabela de events
é usada para enviar notificações push da Web com base no valor do evento. Portanto, faz sentido conectar essa tabela à tabela user_subscription
para poder enviar notificações push nas assinaturas armazenadas nessa tabela.
events.user_id → user_subscription.id
A tabela stock_data
está relacionada à tabela de símbolos como:
stock_data.symbol → symbol.id
Também temos que construir algumas relações na tabela de symbol
como:
stock_data.symbol → symbol.id events.symbol → symbol.id
Já criamos as tabelas necessárias e também estabelecemos as relações entre elas! Vamos mudar para a guia GRAPHIQL
no console para ver a mágica!
Hasura já configurou as consultas do GraphQL com base nestas tabelas:

É muito simples consultar essas tabelas e você também pode aplicar qualquer um desses filtros/propriedades ( distinct_on
, limit
, offset
, order_by
, where
) para obter os dados desejados.
Tudo isso parece bom, mas ainda não conectamos nosso código do lado do servidor ao console Hasura. Vamos completar essa parte!
Conectando o script NodeJs ao banco de dados Postgres
Por favor, coloque as opções necessárias no arquivo config.js
no diretório do 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;
Por favor, coloque essas opções da string do banco de dados que foi gerada quando criamos o banco de dados Postgres no Heroku.
O apiHostOptions
consiste nas opções relacionadas à API, como host
, key
, timeSeriesFunction
e interval
.
Você obterá o campo graphqlURL
na guia GRAPHIQL no console Hasura.
A função getConfig
é usada para retornar o valor solicitado do objeto de configuração. Já usamos isso em index.js
no diretório do server
.
É hora de executar o servidor e preencher alguns dados no banco de dados. Eu adicionei um script no package.json
como:
"scripts": { "start": "nodemon index.js" }
Execute npm start
no terminal e os pontos de dados do array de símbolos em index.js
devem ser preenchidos nas tabelas.
Refatorando a consulta bruta no script NodeJs para a mutação do GraphQL
Agora que o mecanismo Hasura está configurado, vamos ver como pode ser fácil chamar uma mutação na tabela stock_data
.
A função insertStocksData
em queries.js
usa uma consulta bruta:
const query = 'INSERT INTO stock_data (symbol, high, low, open, close, volume, time) VALUES ($1, $2, $3, $4, $5, $6, $7)';
Vamos refatorar essa consulta e usar a mutação desenvolvida pelo mecanismo Hasura. Aqui está o queries.js
refatorado no diretório do 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 }
Observação: temos que adicionar graphqlURL
no arquivo config.js
.
O módulo apollo-fetch
retorna uma função de busca que pode ser usada para consultar/alterar a data no terminal GraphQL. Fácil o suficiente, certo?
A única mudança que temos que fazer no index.js
é retornar o objeto stocks no formato exigido pela função insertStocksData
. Confira index2.js
e queries2.js
para obter o código completo com essa abordagem.
Agora que realizamos o lado dos dados do projeto, vamos passar para o front-end e construir alguns componentes interessantes!
Nota : Não temos que manter as opções de configuração do banco de dados com esta abordagem!
Front-end usando React e cliente Apollo
O projeto front-end está no mesmo repositório e é criado usando o pacote create-react-app
. O service worker gerado usando este pacote oferece suporte ao cache de ativos, mas não permite que mais personalizações sejam adicionadas ao arquivo do service worker. Já existem alguns problemas em aberto para adicionar suporte para opções personalizadas de service worker. Existem maneiras de se livrar desse problema e adicionar suporte para um service worker personalizado.
Vamos começar analisando a estrutura do projeto front-end:

Por favor, verifique o diretório src
! Não se preocupe com os arquivos relacionados ao service worker por enquanto. Aprenderemos mais sobre esses arquivos posteriormente nesta seção. O resto da estrutura do projeto parece simples. A pasta de components
terá os componentes (Loader, Chart); a pasta services
contém algumas das funções/serviços auxiliares usados para transformar objetos na estrutura necessária; styles
como o nome sugere contém os arquivos sass usados para estilizar o projeto; views
é o diretório principal e contém os componentes da camada de visualização.
Precisaríamos de apenas dois componentes de visualização para este projeto — A Lista de Símbolos e a Série Temporal de Símbolos. Construiremos a série temporal usando o componente Chart da biblioteca highcharts. Vamos começar a adicionar código nesses arquivos para construir as peças no front-end!
Instalando dependências
Aqui está a lista de dependências que vamos precisar:
-
apollo-boost
Apollo boost é uma maneira de configuração zero para começar a usar o Apollo Client. Ele vem com as opções de configuração padrão. -
reactstrap
ebootstrap
Os componentes são construídos usando esses dois pacotes. -
graphql
egraphql-type-json
graphql
é uma dependência necessária para usarapollo-boost
egraphql-type-json
é usado para dar suporte ao tipo de dadosjson
que está sendo usado no esquema GraphQL. highcharts
ehighcharts-react-official
E esses dois pacotes serão usados para construir o gráfico:node-sass
Isso é adicionado para dar suporte a arquivos sass para estilização.uuid
Este pacote é usado para gerar valores aleatórios fortes.
Todas essas dependências farão sentido quando começarmos a usá-las no projeto. Vamos para a próxima parte!
Configurando o cliente Apollo
Crie um apolloClient.js
dentro da pasta src
como:
import ApolloClient from 'apollo-boost'; const apolloClient = new ApolloClient({ uri: '<HASURA_CONSOLE_URL>' }); export default apolloClient;
O código acima instancia o ApolloClient e recebe uri
nas opções de configuração. O uri
é o URL do seu console Hasura. Você obterá esse campo uri
na guia GRAPHIQL
na seção GraphQL Endpoint .
O código acima parece simples, mas cuida da parte principal do projeto! Ele conecta o esquema GraphQL construído no Hasura com o projeto atual.
Também temos que passar esse objeto cliente apollo para ApolloProvider
e encapsular o componente raiz dentro ApolloProvider
. Isso permitirá que todos os componentes aninhados dentro do componente principal usem prop do client
e disparem consultas nesse objeto cliente.
Vamos modificar o arquivo 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') );
Por favor, ignore o código relacionado a insertSubscription
. Vamos entender isso em detalhes mais tarde. O resto do código deve ser simples de contornar. A função de render
recebe o componente raiz e o elementId como parâmetros. Observe client
(instância ApolloClient) está sendo passado como prop para ApolloProvider
. Você pode verificar o arquivo index.js
completo aqui.

Configurando o Custom Service Worker
Um Service worker é um arquivo JavaScript que tem a capacidade de interceptar solicitações de rede. Ele é usado para consultar o cache para verificar se o ativo solicitado já está presente no cache em vez de fazer uma viagem até o servidor. Os service workers também são usados para enviar notificações push da Web para os dispositivos inscritos.
Temos que enviar notificações push da web para as atualizações de preços das ações para os usuários inscritos. Vamos preparar o terreno e construir este arquivo de service worker!
O recorte relacionado a insertSubscription
no arquivo index.js
está fazendo o trabalho de registrar o service worker e colocar o objeto de assinatura no banco de dados usando subscriptionMutation
.
Consulte queries.js para todas as consultas e mutações que estão sendo usadas no projeto.
serviceWorker.register(insertSubscription);
invoca a função de register
escrita no arquivo serviceWorker.js
. Aqui está:
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(); }) } }
A função acima verifica primeiro se o serviceWorker
é suportado pelo navegador e, em seguida, registra o arquivo do service worker hospedado na URL swUrl
. Vamos verificar este arquivo em um momento!
A função getSubscription
faz o trabalho de obter o objeto de assinatura usando o método subscribe
no objeto pushManager
. Esse objeto de assinatura é armazenado na tabela user_subscription
em relação a um userId. Observe que o userId está sendo gerado usando a função uuid
. Vamos verificar a função 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 })); }) } }) }
Você pode verificar o arquivo serviceWorker.js
para obter o código completo!

Notification.requestPermission()
invocou este pop-up que pede permissão ao usuário para enviar notificações. Depois que o usuário clica em Permitir, um objeto de assinatura é gerado pelo serviço de push. Estamos armazenando esse objeto no localStorage como:

O endpoint
de campo no objeto acima é usado para identificar o dispositivo e o servidor usa esse endpoint para enviar notificações push da web ao usuário.
Fizemos o trabalho de inicializar e registrar o service worker. Também temos o objeto de assinatura do usuário! Isso está funcionando perfeitamente por causa do arquivo serviceWorker.js
presente na pasta public
. Vamos agora configurar o service worker para preparar as coisas!
Este é um tema um pouco difícil, mas vamos acertar! Conforme mencionado anteriormente, o utilitário create-react-app
não oferece suporte a personalizações por padrão para o service worker. Podemos alcançar a implementação do trabalhador de atendimento ao cliente usando o módulo workbox-build
trabalho.
Também temos que garantir que o comportamento padrão dos arquivos de pré-cache esteja intacto. Modificaremos a parte em que o service worker é compilado no projeto. E o workbox-build ajuda a alcançar exatamente isso! Coisa legal! Vamos simplificar e listar tudo o que precisamos fazer para que o service worker personalizado funcione:
- Manipule o pré-cache de ativos usando
workboxBuild
. - Crie um modelo de service worker para armazenar em cache os ativos.
- Crie o arquivo
sw-precache-config.js
para fornecer opções de configuração personalizadas. - Adicione o script do service worker de compilação na etapa de compilação em
package.json
.
Não se preocupe se tudo isso parecer confuso! O artigo não se concentra em explicar a semântica por trás de cada um desses pontos. Temos que nos concentrar na parte de implementação por enquanto! Tentarei abordar o raciocínio por trás de todo o trabalho para criar um service worker personalizado em outro artigo.
Vamos criar dois arquivos sw-build.js
e sw-custom.js
no diretório src
. Consulte os links para esses arquivos e adicione o código ao seu projeto.
Vamos agora criar o arquivo sw-precache-config.js
no nível raiz e adicionar o seguinte código nesse arquivo:
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' }] }
Vamos também modificar o arquivo package.json
para criar espaço para a criação do arquivo de service worker personalizado:
Adicione estas instruções na seção de scripts
:
"build-sw": "node ./src/sw-build.js", "clean-cra-sw": "rm -f build/precache-manifest.*.js && rm -f build/service-worker.js",
E modifique o script de build
como:
"build": "react-scripts build && npm run build-sw && npm run clean-cra-sw",
A configuração está finalmente feita! Agora temos que adicionar um arquivo de service worker personalizado dentro da pasta 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 adicionar um push
listener para ouvir as notificações push enviadas pelo servidor. A função showNotification
é usada para exibir notificações push da Web para o usuário.
É isso! Terminamos todo o trabalho árduo de configurar um service worker personalizado para lidar com notificações push da Web. Veremos essas notificações em ação assim que construirmos as interfaces de usuário!
Estamos nos aproximando da construção das principais partes do código. Vamos agora começar com a primeira visualização!
Visualização da Lista de Símbolos
O componente App
que está sendo usado na seção anterior é assim:
import React from 'react'; import SymbolList from './views/symbolList'; const App = () => { return <SymbolList />; }; export default App;
É um componente simples que retorna a visualização SymbolList
e o SymbolList
faz todo o trabalho pesado de exibir símbolos em uma interface de usuário bem amarrada.
Vejamos o symbolList.js
dentro da pasta views
:
Consulte o arquivo aqui!
O componente retorna os resultados da função renderSymbols
. E esses dados estão sendo buscados no banco de dados usando o gancho useQuery
como:
const { loading, error, data } = useQuery(symbolsQuery, {variables: { userId }});
A consulta de symbolsQuery
é definida 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 } } } } } `;
Ele recebe userId
e busca os eventos inscritos desse usuário específico para exibir o estado correto do ícone de notificação (ícone de sino que está sendo exibido junto com o título). A consulta também busca os valores máximo e mínimo do estoque. Observe o uso de aggregate
na consulta acima. As consultas de agregação do Hasura fazem o trabalho nos bastidores para buscar os valores agregados como count
, sum
, avg
, max
, min
, etc.
Com base na resposta da chamada GraphQL acima, aqui está a lista de cartões exibidos no front-end:

A estrutura HTML do cartão se parece com isso:
<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 o componente Card
do ReactStrap para renderizar esses cartões. O componente Popover
é usado para exibir as opções baseadas em assinatura:

Quando o usuário clica no ícone de bell
de uma determinada ação, ele pode optar por ser notificado a cada hora ou quando o preço da ação atingir o valor inserido. Veremos isso em ação na seção Events/Time Triggers.
Nota : Chegaremos ao componente StockTimeseries
na próxima seção!
Consulte symbolList.js
para obter o código completo relacionado ao componente da lista de ações.
Exibição de série temporal de ações
O componente StockTimeseries
usa a 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 } } `;
A consulta acima busca os 25 pontos de dados recentes da ação selecionada. Por exemplo, aqui está o gráfico da métrica de abertura de ações do Facebook:

Este é um componente direto onde passamos algumas opções de gráfico para o componente [ HighchartsReact
]. Aqui estão as opções do 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') } ] }
O eixo X mostra a hora e o eixo Y mostra o valor métrico naquele momento. A função getDataPoints
é usada para gerar uma série de pontos para cada uma das séries.
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; }
Simples! É assim que o componente Chart é gerado! Consulte os arquivos Chart.js e stockTimeseries.js
para obter o código completo das séries temporais de ações.
Agora você deve estar pronto com os dados e as interfaces de usuário do projeto. Vamos agora passar para a parte interessante — configurar acionadores de evento/tempo com base na entrada do usuário.
Configurando acionadores de evento/agendados
Nesta seção, aprenderemos como configurar gatilhos no console Hasura e como enviar notificações push da Web para os usuários selecionados. Vamos começar!
Acionadores de eventos no console Hasura
Vamos criar um trigger de evento stock_value
na tabela stock_data
e insert
como a operação de trigger. O webhook será executado sempre que houver uma inserção na tabela stock_data
.

Vamos criar um projeto de falha para o URL do webhook. Deixe-me colocar um pouco sobre webhooks para facilitar o entendimento:
Os webhooks são usados para enviar dados de um aplicativo para outro na ocorrência de um determinado evento. Quando um evento é acionado, uma chamada HTTP POST é feita para a URL do webhook com os dados do evento como carga útil.
Nesse caso, quando houver uma operação de inserção na tabela stock_data
, será feita uma pós chamada HTTP para a URL do webhook configurada (pós chamada no projeto glitch).
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!
Buscando usuários inscritos
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()); }
Na função handleStockValueTrigger
acima, primeiro buscamos os usuários inscritos usando a função getSubscribedUsers
. Em seguida, estamos enviando notificações push da Web para cada um desses usuários. A função sendWebpush
é usada para enviar a notificação. Veremos a implementação do web-push em breve.
A função getSubscribedUsers
usa a 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 } } }
Essa consulta recebe o símbolo de ações e o valor e busca os detalhes do usuário, incluindo user-id
e user_subscription
que correspondem a estas condições:
-
symbol
igual ao que está sendo passado na carga útil. -
trigger_type
é igual aevent
. -
trigger_value
é maior ou igual ao que está sendo passado para esta função (close
neste caso).
Assim que obtivermos a lista de usuários, a única coisa que resta é enviar notificações por push da Web para eles! Vamos fazer isso imediatamente!
Enviando Notificações Web-Push aos Usuários Assinados
Primeiro, precisamos obter as chaves VAPID pública e privada para enviar notificações por push da Web. Armazene essas chaves no arquivo .env
e defina esses detalhes em 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)) }
A função sendNotification
é usada para enviar o web-push no endpoint de assinatura fornecido como o primeiro parâmetro.
Isso é tudo o que é necessário para enviar com êxito as notificações push da Web para os usuários inscritos. Aqui está o código completo definido em 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"); });
Vamos testar esse fluxo assinando estoque com algum valor e inserindo manualmente esse valor na tabela (para teste)!
AMZN
com valor de 2000
e depois inseri um ponto de dados na tabela com esse valor. Veja como o aplicativo notificador de ações me notificou logo após a inserção:

Arrumado! Você também pode verificar o log de invocação de eventos aqui:

O webhook está fazendo o trabalho conforme o esperado! Estamos todos prontos para os gatilhos do evento agora!
Acionadores Agendados/Cron
Podemos obter um gatilho baseado em tempo para notificar os usuários assinantes a cada hora usando o gatilho de evento Cron como:

Podemos usar o mesmo URL do webhook e lidar com os usuários inscritos com base no tipo de evento de gatilho como stock_price_time_based_trigger
. A implementação é semelhante ao gatilho baseado em evento.
Conclusão
Neste artigo, construímos um aplicativo de notificação de preços de ações. Aprendemos como buscar preços usando as APIs Alpha Vantage e armazenar os pontos de dados no banco de dados Postgres com suporte de Hasura. Também aprendemos como configurar o mecanismo Hasura GraphQL e criar gatilhos programados e baseados em eventos. Criamos um projeto de falha para enviar notificações push da Web para os usuários inscritos.