openweather-api-node
Version:
Simple package that makes it easy to work with OpenWeather API
553 lines (552 loc) • 23.2 kB
JavaScript
"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);