UNPKG

openweather-api-node

Version:

Simple package that makes it easy to work with OpenWeather API

553 lines (552 loc) 23.2 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __exportStar = (this && this.__exportStar) || function(m, exports) { for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.OpenWeatherAPI = void 0; const request_1 = require("./request"); const current_parser_1 = __importDefault(require("./parsers/weather/current-parser")); const forecast_parser_1 = __importDefault(require("./parsers/weather/forecast-parser")); const current_parser_2 = __importDefault(require("./parsers/onecall/current-parser")); const minutely_parser_1 = __importDefault(require("./parsers/onecall/minutely-parser")); const hourly_parser_1 = __importDefault(require("./parsers/onecall/hourly-parser")); const daily_parser_1 = __importDefault(require("./parsers/onecall/daily-parser")); const history_parser_1 = __importDefault(require("./parsers/onecall/history-parser")); const single_parser_1 = __importDefault(require("./parsers/air-pollution/single-parser")); const list_parser_1 = __importDefault(require("./parsers/air-pollution/list-parser")); const constants_1 = require("./constants"); function isObject(x) { return Boolean(x) && typeof x === "object" && !Array.isArray(x); } // https://stackoverflow.com/questions/27936772/how-to-deep-merge-instead-of-shallow-merge function mergeObj(target, ...sources) { if (!sources.length) return target; const source = sources.shift(); if (isObject(target) && isObject(source)) { for (const key in source) { if (isObject(source[key])) { if (!target[key]) Object.assign(target, { [key]: {} }); mergeObj(target[key], source[key]); } else { Object.assign(target, { [key]: source[key] }); } } } return mergeObj(target, ...sources); } class OpenWeatherAPI { /** * Constructor of the class. You can specify global options here * * @constructor * @param globalOptions - object that defines global options * @returns OpenWeatherAPI object */ constructor(globalOptions = {}) { if (!isObject(globalOptions)) throw new Error("Provide {} object as options"); this.globalOptions = {}; const go = globalOptions; for (const key in go) { if (Object.hasOwnProperty.call(go, key)) { const value = go[key]; switch (key) { case "key": this.setKey(value); break; // @ts-ignore: Type '"language"' is not comparable to type 'keyof Options'. case "language": case "lang": this.setLanguage(value); break; case "units": this.setUnits(value); break; case "locationName": this.setLocationByName(value); break; case "coordinates": this.setLocationByCoordinates(value.lat, value.lon); break; case "zipCode": this.setLocationByZipCode(value); break; default: throw new Error("Unknown parameter: " + key); } } } } /** * Sets global API key * * @param key - api key */ setKey(key) { if (!key) throw new Error("Empty value cannot be a key: " + key); this.globalOptions.key = key; } /** * Getter for global key * * @returns global API key */ getKey() { return this.globalOptions.key; } /** * Sets global language (Language must be listed [here](https://openweathermap.org/current#multi)) * * @param lang - language */ setLanguage(lang) { this.globalOptions.lang = this.evaluateLanguage(lang); } /** * Getter for global language * * @return global language */ getLanguage() { return this.globalOptions.lang; } evaluateLanguage(lang) { if (typeof lang !== "string") throw new Error("language needs to be a string"); const loweredLang = lang.toLowerCase(); if (constants_1.SUP_LANGS.includes(loweredLang)) return loweredLang; else throw new Error("Unsupported language: " + loweredLang); } /** * Sets global units * * @param units - units (Only **standard**, **metric** or **imperial** are supported) */ setUnits(units) { this.globalOptions.units = this.evaluateUnits(units); } /** * Getter for global units * * @returns global units */ getUnits() { return this.globalOptions.units; } evaluateUnits(units) { if (typeof units !== "string") throw new Error("units needs to be a string"); const loweredUnits = units.toLowerCase(); if (constants_1.SUP_UNITS.includes(loweredUnits)) return loweredUnits; else throw new Error("Unsupported units: " + loweredUnits); } /** * Sets global location by provided name * * @param name - name of the location (`q` parameter [here](https://openweathermap.org/api/geocoding-api#direct_name)) */ setLocationByName(name) { if (!name) throw new Error("Empty value cannot be a location name: " + name); this.globalOptions.coordinates = undefined; this.globalOptions.zipCode = undefined; this.globalOptions.locationName = name; } async evaluateLocationByName(name, key) { if (typeof name !== "string") throw new Error("name of the location needs to be a string"); let response = await this.fetch(`${constants_1.API_ENDPOINT}${constants_1.GEO_PATH}direct?q=${encodeURIComponent(name)}&limit=1&appid=${encodeURIComponent(key)}`); let data = response.data; if (data.length == 0) throw new Error("Unknown location name: " + name); data = response.data[0]; return { lat: data.lat, lon: data.lon, }; } /** * Sets global location by provided coordinates * * @param lat - latitude of the location * @param lon - longitude of the location */ setLocationByCoordinates(lat, lon) { let location = this.evaluateLocationByCoordinates({ lat, lon }); this.globalOptions.locationName = undefined; this.globalOptions.zipCode = undefined; this.globalOptions.coordinates = { lat: location.lat, lon: location.lon }; } evaluateLocationByCoordinates(coords) { if (!isObject(coords) || typeof coords.lat !== "number" || typeof coords.lon !== "number") throw new Error("Invalid Coordinates"); const { lat, lon } = coords; if (-90 <= lat && lat <= 90 && -180 <= lon && lon <= 180) { return { lat, lon }; } else { throw new Error("Invalid Coordinates: lat must be between -90 & 90 and lon must be between -180 & 180"); } } /** * Sets global location by provided zip/post code * * @param zipCode - zip/post code and country code divided by comma (`zip` parameter [here](https://openweathermap.org/api/geocoding-api#direct_zip)) */ setLocationByZipCode(zipCode) { if (!zipCode) throw new Error("Empty value cannot be a location zip code: " + zipCode); this.globalOptions.coordinates = undefined; this.globalOptions.locationName = undefined; this.globalOptions.zipCode = zipCode; } async evaluateLocationByZipCode(zipCode, key) { if (typeof zipCode !== "string") throw new Error("zip code needs to be a string"); let response = await this.fetch(`${constants_1.API_ENDPOINT}${constants_1.GEO_PATH}zip?zip=${encodeURIComponent(zipCode)}&appid=${encodeURIComponent(key)}`); let data = response.data; return { lat: data.lat, lon: data.lon, }; } /** * Getter for location * * @param options - options used only for this call * @returns location or null for no location */ async getLocation(options = {}) { const parsedOptions = await this.parseOptions(options); let response = await this.fetch(`${constants_1.API_ENDPOINT}${constants_1.GEO_PATH}reverse?lat=${parsedOptions.coordinates?.lat}&lon=${parsedOptions.coordinates?.lon}&limit=1&appid=${encodeURIComponent(parsedOptions.key || "")}`); let data = response.data; return data.length ? data[0] : null; } /** * Getter for locations from query * * @param query - query used to search the locations (`q` parameter [here](https://openweathermap.org/api/geocoding-api#direct_name)) * @param options - options used only for this call * @returns all found locations */ async getAllLocations(query, options = {}) { if (!query) throw new Error("No query"); const parsedOptions = await this.parseOptions(options); let response = await this.fetch(`${constants_1.API_ENDPOINT}${constants_1.GEO_PATH}direct?q=${encodeURIComponent(query)}&limit=5&appid=${encodeURIComponent(parsedOptions.key || "")}`); let data = response.data; return data; } // Weather getters /** * Getter for current weather * * @param options - options used only for this call * @returns weather object of current weather */ async getCurrent(options = {}) { await this.uncacheLocation(); const parsedOptions = await this.parseOptions(options); let response = await this.fetch(this.createURL(parsedOptions, constants_1.WEATHER_PATH)); let data = response.data; return (0, current_parser_1.default)(data); } /** * Getter for forecasted weather * * @param limit - maximum length of returned array * @param options - options used only for this call * @returns array of Weather objects, one for every 3 hours, up to 5 days */ async getForecast(limit = Number.POSITIVE_INFINITY, options = {}) { await this.uncacheLocation(); const parsedOptions = await this.parseOptions(options); let response = await this.fetch(this.createURL(parsedOptions, constants_1.FORECAST_PATH)); let data = response.data; return (0, forecast_parser_1.default)(data, limit); } /** * Getter for minutely weather * * @param limit - maximum length of returned array * @param options - options used only for this call * @returns array of Weather objects, one for every next minute (Empty if API returned no info about minutely weather) */ async getMinutelyForecast(limit = Number.POSITIVE_INFINITY, options = {}) { await this.uncacheLocation(); const parsedOptions = await this.parseOptions(options); let response = await this.fetch(this.createURL(parsedOptions, constants_1.ONECALL_PATH, { exclude: "alerts,current,hourly,daily", })); let data = response.data; return (0, minutely_parser_1.default)(data, limit); } /** * Getter for hourly weather * * @param limit - maximum length of returned array * @param options - options used only for this call * @returns array of Weather objects, one for every next hour (Empty if API returned no info about hourly weather) */ async getHourlyForecast(limit = Number.POSITIVE_INFINITY, options = {}) { await this.uncacheLocation(); const parsedOptions = await this.parseOptions(options); let response = await this.fetch(this.createURL(parsedOptions, constants_1.ONECALL_PATH, { exclude: "alerts,current,minutely,daily", })); let data = response.data; return (0, hourly_parser_1.default)(data, limit); } /** * Getter for daily weather * * @param limit - maximum length of returned array * @param includeToday - boolean indicating whether to include today's weather in returned array * @param options - options used only for this call * @returns array of Weather objects, one for every next day (Empty if API returned no info about daily weather) */ async getDailyForecast(limit = Number.POSITIVE_INFINITY, includeToday = false, options = {}) { await this.uncacheLocation(); const parsedOptions = await this.parseOptions(options); let response = await this.fetch(this.createURL(parsedOptions, constants_1.ONECALL_PATH, { exclude: "alerts,current,minutely,hourly", })); let data = response.data; if (!includeToday) data.daily.shift(); return (0, daily_parser_1.default)(data, limit); } /** * Getter for today's weather * * @param options - options used only for this call * @returns weather object of today's weather **NOT the same as current!** */ async getToday(options = {}) { return (await this.getDailyForecast(1, true, options))[0]; } /** * Getter for alerts\ * **Note:** some agencies provide the alert’s description only in a local language. * * @param options - options used only for this call * @returns alerts */ async getAlerts(options = {}) { await this.uncacheLocation(); const parsedOptions = await this.parseOptions(options); let response = await this.fetch(this.createURL(parsedOptions, constants_1.ONECALL_PATH, { exclude: "current,minutely,hourly,daily", })); let data = response.data; return data.alerts ?? []; } /** * Getter for every type of weather call and alerts * * @param options - options used only for this call * @returns object that contains everything */ async getEverything(options = {}) { await this.uncacheLocation(); const parsedOptions = await this.parseOptions(options); let response = await this.fetch(this.createURL(parsedOptions, constants_1.ONECALL_PATH)); let data = response.data; return { lat: data.lat, lon: data.lon, timezone: data.timezone, timezoneOffset: data.timezone_offset, current: (0, current_parser_2.default)(data), minutely: (0, minutely_parser_1.default)(data, Number.POSITIVE_INFINITY), hourly: (0, hourly_parser_1.default)(data, Number.POSITIVE_INFINITY), daily: (0, daily_parser_1.default)(data, Number.POSITIVE_INFINITY), alerts: data.alerts, }; } /** * Getter for historical data about weather * * @param dt - Date from the **previous five days** (Unix time, UTC time zone) * @param options - options used only for this call */ async getHistory(dt, options = {}) { if (dt === undefined) throw new Error("Provide time"); await this.uncacheLocation(); dt = Math.round(new Date(dt).getTime() / 1000); const parsedOptions = await this.parseOptions(options); let response = await this.fetch(this.createURL(parsedOptions, constants_1.ONECALL_PATH + "/timemachine", { dt: dt.toString(), })); let data = response.data; return (0, history_parser_1.default)(data); } // Uncategorized Methods /** * Getter for current data about air pollution * * @param options - options used only for this call * @returns Air Pollution Object with data about current pollution */ async getCurrentAirPollution(options = {}) { await this.uncacheLocation(); const parsedOptions = await this.parseOptions(options); let response = await this.fetch(this.createURL(parsedOptions, constants_1.AIR_POLLUTION_PATH)); let data = response.data; return (0, single_parser_1.default)(data); } /** * Getter for future data about air pollution * * @param limit - maximum length of returned array * @param options - options used only for this call * @returns Array of Air Pollution Objects with data about future pollution */ async getForecastedAirPollution(limit = Number.POSITIVE_INFINITY, options = {}) { await this.uncacheLocation(); const parsedOptions = await this.parseOptions(options); let response = await this.fetch(this.createURL(parsedOptions, constants_1.AIR_POLLUTION_PATH + "/forecast")); let data = response.data; return (0, list_parser_1.default)(data, limit); } /** * Getter for historical data about air pollution * WARNING: Historical data is accessible from 27th November 2020 * * @param from - Start date (unix time, UTC time zone) * @param to - End date (unix time, UTC time zone) * @param options - options used only for this call * @returns Array of Air Pollution Objects with data about historical pollution */ async getHistoryAirPollution(from, to, options = {}) { await this.uncacheLocation(); const parsedOptions = await this.parseOptions(options); let response = await this.fetch(this.createURL(parsedOptions, constants_1.AIR_POLLUTION_PATH + "/history", { start: Math.round(new Date(from).getTime() / 1000).toString(), end: Math.round(new Date(to).getTime() / 1000).toString(), })); const data = response.data; return (0, list_parser_1.default)(data, Number.POSITIVE_INFINITY); } // helpers async uncacheLocation() { if (typeof this.globalOptions.coordinates?.lat == "number" && typeof this.globalOptions.coordinates?.lon == "number") return; const key = this.globalOptions.key; if (!key) return; try { if (this.globalOptions.locationName) { this.globalOptions.coordinates = await this.evaluateLocationByName(this.globalOptions.locationName, key); } else if (this.globalOptions.zipCode) { this.globalOptions.coordinates = await this.evaluateLocationByZipCode(this.globalOptions.zipCode, key); } } catch { } } createURL(options, path = "", additionalParams = {}) { if (!options.key) throw new Error("Invalid key"); if (!options.coordinates) throw new Error("Invalid coordinates"); let url = new URL(path, constants_1.API_ENDPOINT); url.searchParams.append("appid", options.key); url.searchParams.append("lat", options.coordinates.lat.toString()); url.searchParams.append("lon", options.coordinates.lon.toString()); if (options.lang) url.searchParams.append("lang", options.lang); if (options.units) url.searchParams.append("units", options.units); for (const [key, value] of Object.entries(additionalParams)) { url.searchParams.append(key, value); } return url.href; } async fetch(url) { const res = await (0, request_1.get)(url); const data = res.data; if (data.cod && parseInt(data.cod) !== 200) { throw new Error(JSON.stringify(data)); } else { return res; } } async parseOptions(options) { if (!isObject(options)) throw new Error("Provide {} object as options"); const parsedOptions = {}; for (const key in options) { if (Object.hasOwnProperty.call(options, key)) { const value = options[key]; switch (key) { case "key": { if (typeof value !== "string") throw Error("key needs to be a string"); parsedOptions.key = value; break; } // @ts-ignore: Type '"language"' is not comparable to type 'keyof Options'. case "language": case "lang": { parsedOptions.lang = this.evaluateLanguage(value); break; } case "units": { parsedOptions.units = this.evaluateUnits(value); break; } case "locationName": { parsedOptions.locationName = value; parsedOptions.coordinates = await this.evaluateLocationByName(value, options.key || this.globalOptions.key || ""); break; } case "coordinates": { parsedOptions.coordinates = this.evaluateLocationByCoordinates(value); break; } case "zipCode": { parsedOptions.zipCode = value; parsedOptions.coordinates = await this.evaluateLocationByZipCode(value, options.key || this.globalOptions.key || ""); break; } default: { throw new Error("Unknown parameter: " + key); } } } } return mergeObj({}, this.globalOptions, parsedOptions); } } exports.OpenWeatherAPI = OpenWeatherAPI; exports.default = OpenWeatherAPI; __exportStar(require("./types"), exports);