From adc4aaed0f7275503bf4da2c01b2f67732a30a20 Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 17 May 2021 18:23:54 +0200 Subject: [PATCH] Created Cron job to get data from external api every 15 minutes and save it to local database. Created Weather model and controller to get latest weather status --- controllers/weather.js | 18 +++++++ models/Weather.js | 15 ++++++ package-lock.json | 93 +++++++++++++++++++++++++++++++++++++ package.json | 2 + routes/weather.js | 12 +++++ server.js | 2 + utils/getExternalWeather.js | 42 +++++++++++++++++ utils/jobs.js | 11 +++++ 8 files changed, 195 insertions(+) create mode 100644 controllers/weather.js create mode 100644 models/Weather.js create mode 100644 routes/weather.js create mode 100644 utils/getExternalWeather.js create mode 100644 utils/jobs.js diff --git a/controllers/weather.js b/controllers/weather.js new file mode 100644 index 0000000..66c63de --- /dev/null +++ b/controllers/weather.js @@ -0,0 +1,18 @@ +const asyncWrapper = require('../middleware/asyncWrapper'); +const ErrorResponse = require('../utils/ErrorResponse'); +const Weather = require('../models/Weather'); + +// @desc Get latest weather status +// @route POST /api/weather +// @access Public +exports.getWeather = asyncWrapper(async (req, res, next) => { + const weather = await Weather.findAll({ + order: [['createdAt', 'DESC']], + limit: 1 + }); + + res.status(200).json({ + success: true, + data: weather + }) +}) \ No newline at end of file diff --git a/models/Weather.js b/models/Weather.js new file mode 100644 index 0000000..5611109 --- /dev/null +++ b/models/Weather.js @@ -0,0 +1,15 @@ +const { DataTypes } = require('sequelize'); +const { sequelize } = require('../db'); + +const Weather = sequelize.define('Weather', { + externalLastUpdate: DataTypes.STRING, + tempC: DataTypes.FLOAT, + tempF: DataTypes.FLOAT, + isDay: DataTypes.INTEGER, + conditionText: DataTypes.TEXT, + conditionCode: DataTypes.INTEGER +}, { + freezeTableName: true +}); + +module.exports = Weather; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 9610e46..9d9cbf5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -276,6 +276,14 @@ "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", "optional": true }, + "axios": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz", + "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==", + "requires": { + "follow-redirects": "^1.10.0" + } + }, "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -393,6 +401,15 @@ } } }, + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, "camelcase": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", @@ -633,6 +650,15 @@ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, + "cron-parser": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-3.5.0.tgz", + "integrity": "sha512-wyVZtbRs6qDfFd8ap457w3XVntdvqcwBGxBoTvJQH9KGVKL/fB+h2k3C8AqiVxvUQKN1Ps/Ns46CNViOpVDhfQ==", + "requires": { + "is-nan": "^1.3.2", + "luxon": "^1.26.0" + } + }, "crypto-random-string": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", @@ -681,6 +707,14 @@ "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==", "dev": true }, + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "requires": { + "object-keys": "^1.0.12" + } + }, "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -885,6 +919,11 @@ "unpipe": "~1.0.0" } }, + "follow-redirects": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.1.tgz", + "integrity": "sha512-HWqDgT7ZEkqRzBvc2s64vSZ/hfOceEol3ac/7tKwzuvEyWx3/4UegXh5oBOIotkGsObyk3xznnSRVADBgWSQVg==" + }, "forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", @@ -1002,6 +1041,16 @@ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" }, + "get-intrinsic": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", + "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1" + } + }, "get-stream": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", @@ -1104,6 +1153,11 @@ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" }, + "has-symbols": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", + "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==" + }, "has-unicode": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", @@ -1273,6 +1327,15 @@ "is-path-inside": "^3.0.1" } }, + "is-nan": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz", + "integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==", + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3" + } + }, "is-npm": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-4.0.0.tgz", @@ -1405,6 +1468,11 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "long-timeout": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/long-timeout/-/long-timeout-0.1.1.tgz", + "integrity": "sha1-lyHXiLR+C8taJMLivuGg2lXatRQ=" + }, "lowercase-keys": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", @@ -1419,6 +1487,11 @@ "yallist": "^4.0.0" } }, + "luxon": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-1.27.0.tgz", + "integrity": "sha512-VKsFsPggTA0DvnxtJdiExAucKdAnwbCCNlMM5ENvHlxubqWd0xhZcdb4XgZ7QFNhaRhilXCFxHuoObP5BNA4PA==" + }, "make-dir": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", @@ -1657,6 +1730,16 @@ } } }, + "node-schedule": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/node-schedule/-/node-schedule-2.0.0.tgz", + "integrity": "sha512-cHc9KEcfiuXxYDU+HjsBVo2FkWL1jRAUoczFoMIzRBpOA4p/NRHuuLs85AWOLgKsHtSPjN8csvwIxc2SqMv+CQ==", + "requires": { + "cron-parser": "^3.1.0", + "long-timeout": "0.1.1", + "sorted-array-functions": "^1.3.0" + } + }, "nodemon": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.7.tgz", @@ -1774,6 +1857,11 @@ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" + }, "on-finished": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", @@ -2253,6 +2341,11 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==" }, + "sorted-array-functions": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/sorted-array-functions/-/sorted-array-functions-1.3.0.tgz", + "integrity": "sha512-2sqgzeFlid6N4Z2fUQ1cvFmTOLRi/sEDzSQ0OKYchqgoPmQBVyM3959qYx3fpS6Esef80KjmpgPeEr028dP3OA==" + }, "spawn-command": { "version": "0.0.2-1", "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2-1.tgz", diff --git a/package.json b/package.json index 425063e..c3e063f 100644 --- a/package.json +++ b/package.json @@ -13,10 +13,12 @@ "license": "ISC", "dependencies": { "@types/express": "^4.17.11", + "axios": "^0.21.1", "colors": "^1.4.0", "concurrently": "^6.0.2", "dotenv": "^9.0.0", "express": "^4.17.1", + "node-schedule": "^2.0.0", "sequelize": "^6.6.2", "sqlite3": "^5.0.2" }, diff --git a/routes/weather.js b/routes/weather.js new file mode 100644 index 0000000..1bc264a --- /dev/null +++ b/routes/weather.js @@ -0,0 +1,12 @@ +const express = require('express'); +const router = express.Router(); + +const { + getWeather +} = require('../controllers/weather'); + +router + .route('') + .get(getWeather); + +module.exports = router; \ No newline at end of file diff --git a/server.js b/server.js index ed43611..d9e510f 100644 --- a/server.js +++ b/server.js @@ -1,6 +1,7 @@ const express = require('express'); const { connectDB } = require('./db'); const errorHandler = require('./middleware/errorHandler'); +const jobs = require('./utils/jobs'); const colors = require('colors'); require('dotenv').config(); @@ -19,6 +20,7 @@ app.use(express.json()); // Link controllers with routes app.use('/api/apps', require('./routes/apps')); app.use('/api/config', require('./routes/config')); +app.use('/api/weather', require('./routes/weather')); // Custom error handler app.use(errorHandler); diff --git a/utils/getExternalWeather.js b/utils/getExternalWeather.js new file mode 100644 index 0000000..f265e20 --- /dev/null +++ b/utils/getExternalWeather.js @@ -0,0 +1,42 @@ +const Config = require('../models/Config'); +const Weather = require('../models/Weather'); +const axios = require('axios'); + +const getExternalWeather = async () => { + // Get API key from database + let secret = await Config.findOne({ + where: { key: 'WEATHER_API_KEY' } + }); + + if (!secret) { + console.log('API key was not found'); + return; + } + + secret = secret.value; + + // Fetch data from external API + try { + const res = await axios.get(`http://api.weatherapi.com/v1/current.json?key=${secret}&q=52.229676,21.012229`); + + // For dev + // console.log(res.data); + + // Save weather data + const cursor = res.data.current; + await Weather.create({ + externalLastUpdate: cursor.last_updated, + tempC: cursor.temp_c, + tempF: cursor.temp_f, + isDay: cursor.is_day, + conditionText: cursor.condition.text, + conditionCode: cursor.condition.code + }); + } catch (err) { + console.log(err); + console.log('External API request failed'); + return; + } +} + +module.exports = getExternalWeather; \ No newline at end of file diff --git a/utils/jobs.js b/utils/jobs.js new file mode 100644 index 0000000..1243f2f --- /dev/null +++ b/utils/jobs.js @@ -0,0 +1,11 @@ +const schedule = require('node-schedule'); +const getExternalWeather = require('./getExternalWeather'); + +const weatherJob = schedule.scheduleJob('updateWeather', '0 */15 * * * *', async () => { + try { + await getExternalWeather(); + console.log('weather updated'); + } catch (err) { + console.log(err); + } +}) \ No newline at end of file