Release 2.9.0
This commit is contained in:
@@ -59,7 +59,7 @@ function buildAuthHeaders(source) {
|
||||
|
||||
async function fetchApiSourcesData(pool) {
|
||||
const [apiSources] = await pool.query(
|
||||
'SELECT id, name, api_url, request_method, request_body_json, auth_method, auth_username, auth_password, auth_bearer_token, auth_header_name, auth_header_value, token_url, token_request_body_json, token_response_path, token_header_name, token_header_prefix, items_path, update_interval_value, update_interval_unit, last_pulled_at, last_pull_error, last_response_status, last_response_content_type, last_response_json, created_at, modified_at, created_by, modified_by FROM i_api_sources ORDER BY modified_at DESC, id DESC'
|
||||
'SELECT id, name, api_url, request_method, request_body_json, auth_method, auth_username, auth_password, auth_bearer_token, auth_header_name, auth_header_value, token_url, token_request_body_json, token_response_path, token_header_name, token_header_prefix, items_path, enabled, update_interval_value, update_interval_unit, last_pulled_at, last_pull_error, last_response_status, last_response_content_type, last_response_json, created_at, modified_at, created_by, modified_by FROM i_api_sources ORDER BY modified_at DESC, id DESC'
|
||||
);
|
||||
|
||||
return { apiSources: apiSources };
|
||||
@@ -67,7 +67,7 @@ async function fetchApiSourcesData(pool) {
|
||||
|
||||
async function fetchApiSourcesPage(pool, page, pageSize, searchTerm, sortKey, sortDirection) {
|
||||
const paged = await fetchPagedRows(pool, {
|
||||
selectSql: 'SELECT id, name, api_url, request_method, request_body_json, auth_method, auth_username, auth_password, auth_bearer_token, auth_header_name, auth_header_value, token_url, token_request_body_json, token_response_path, token_header_name, token_header_prefix, items_path, update_interval_value, update_interval_unit, last_pulled_at, last_pull_error, last_response_status, last_response_content_type, last_response_json, created_at, modified_at, created_by, modified_by FROM i_api_sources ORDER BY modified_at DESC, id DESC',
|
||||
selectSql: 'SELECT id, name, api_url, request_method, request_body_json, auth_method, auth_username, auth_password, auth_bearer_token, auth_header_name, auth_header_value, token_url, token_request_body_json, token_response_path, token_header_name, token_header_prefix, items_path, enabled, update_interval_value, update_interval_unit, last_pulled_at, last_pull_error, last_response_status, last_response_content_type, last_response_json, created_at, modified_at, created_by, modified_by FROM i_api_sources ORDER BY modified_at DESC, id DESC',
|
||||
countSql: 'SELECT COUNT(*) AS count FROM i_api_sources',
|
||||
searchColumns: ['name', 'api_url', 'last_pull_error'],
|
||||
searchTerm: searchTerm,
|
||||
@@ -91,7 +91,7 @@ async function fetchApiSourcesPage(pool, page, pageSize, searchTerm, sortKey, so
|
||||
|
||||
async function fetchApiSourceById(pool, id) {
|
||||
const [rows] = await pool.query(
|
||||
'SELECT id, name, api_url, request_method, request_body_json, auth_method, auth_username, auth_password, auth_bearer_token, auth_header_name, auth_header_value, token_url, token_request_body_json, token_response_path, token_header_name, token_header_prefix, items_path, update_interval_value, update_interval_unit, last_pulled_at, last_pull_error, last_response_status, last_response_content_type, last_response_json, created_at, modified_at, created_by, modified_by FROM i_api_sources WHERE id = ?',
|
||||
'SELECT id, name, api_url, request_method, request_body_json, auth_method, auth_username, auth_password, auth_bearer_token, auth_header_name, auth_header_value, token_url, token_request_body_json, token_response_path, token_header_name, token_header_prefix, items_path, enabled, update_interval_value, update_interval_unit, last_pulled_at, last_pull_error, last_response_status, last_response_content_type, last_response_json, created_at, modified_at, created_by, modified_by FROM i_api_sources WHERE id = ?',
|
||||
[id]
|
||||
);
|
||||
|
||||
|
||||
@@ -40,6 +40,8 @@ const SETTING_DEFINITIONS = [
|
||||
,{ key: 'data-sources.rss_default_interval_unit', type: 'enum', values: ['seconds', 'minutes', 'hours'], defaultValue: 'minutes' }
|
||||
,{ key: 'data-sources.api_default_interval_value', type: 'integer', min: 1, defaultValue: 60 }
|
||||
,{ key: 'data-sources.api_default_interval_unit', type: 'enum', values: ['seconds', 'minutes', 'hours'], defaultValue: 'minutes' }
|
||||
,{ key: 'weather.open_meteo_api_key', type: 'string', defaultValue: '' }
|
||||
,{ key: 'weather.pirate_weather_api_key', type: 'string', defaultValue: '' }
|
||||
];
|
||||
|
||||
const DEFINITIONS_BY_KEY = new Map(SETTING_DEFINITIONS.map(function (definition) {
|
||||
|
||||
@@ -7,6 +7,7 @@ const { fetchPlaylistById } = require('./playlists');
|
||||
const { normalizeDisplayMode, fetchTimetablesData, fetchTimetableGroupsPage, fetchTimetableGroupById, fetchTimetableEntriesByGroupId, buildTimetableGroupPayload } = require('./timetables');
|
||||
const { fetchApiSourcesData, fetchApiSourcesPage, fetchApiSourceById, fetchApiSourceResponse, buildApiSourcePayload } = require('./api-sources');
|
||||
const { fetchRssFeedsData, fetchRssFeedsPage, fetchRssFeedById, fetchRssFeedItemsByFeedId, normalizeRssFeedItem, buildRssFeedPayload, fetchRssFeedItems, replaceRssFeedItems } = require('./rss-feeds');
|
||||
const { fetchWeatherLocationsData, fetchWeatherLocationsPage, fetchWeatherLocationById, fetchWeatherLocationSuggestions, fetchWeatherLocationForecast, buildWeatherLocationPayload } = require('./weather');
|
||||
const { slugify, uniqueScreenSlug, fetchScreenById, fetchScreenEditData, fetchScreenPlayerUrls, fetchPlayerPublicBaseUrl, fetchScreenPlayerRecord, fetchPlayerRecordByIdentifier } = require('./screens');
|
||||
const { fetchPlayerRegistrations } = require('./player-registry');
|
||||
const { fetchTemplateById, fetchTemplatesData, extractTemplateRegions, buildTemplatePayload } = require('./templates');
|
||||
@@ -59,6 +60,12 @@ module.exports = {
|
||||
buildRssFeedPayload,
|
||||
fetchRssFeedItems,
|
||||
replaceRssFeedItems,
|
||||
fetchWeatherLocationsData,
|
||||
fetchWeatherLocationsPage,
|
||||
fetchWeatherLocationById,
|
||||
fetchWeatherLocationSuggestions,
|
||||
fetchWeatherLocationForecast,
|
||||
buildWeatherLocationPayload,
|
||||
fetchScreenById,
|
||||
fetchScreenEditData,
|
||||
fetchScreenPlayerUrls,
|
||||
|
||||
@@ -17,7 +17,7 @@ function normalizeUpdateIntervalUnit(value) {
|
||||
|
||||
async function fetchRssFeedsData(pool) {
|
||||
const [rssFeeds] = await pool.query(
|
||||
'SELECT id, name, feed_url, update_interval_value, update_interval_unit, item_limit, created_at, modified_at, created_by, modified_by FROM i_rss_feeds ORDER BY modified_at DESC, id DESC'
|
||||
'SELECT id, name, feed_url, enabled, update_interval_value, update_interval_unit, item_limit, last_pulled_at, created_at, modified_at, created_by, modified_by FROM i_rss_feeds ORDER BY modified_at DESC, id DESC'
|
||||
);
|
||||
|
||||
return { rssFeeds: rssFeeds };
|
||||
@@ -25,7 +25,7 @@ async function fetchRssFeedsData(pool) {
|
||||
|
||||
async function fetchRssFeedsPage(pool, page, pageSize, searchTerm, sortKey, sortDirection) {
|
||||
const paged = await fetchPagedRows(pool, {
|
||||
selectSql: 'SELECT id, name, feed_url, update_interval_value, update_interval_unit, item_limit, created_at, modified_at, created_by, modified_by FROM i_rss_feeds ORDER BY modified_at DESC, id DESC',
|
||||
selectSql: 'SELECT id, name, feed_url, enabled, update_interval_value, update_interval_unit, item_limit, last_pulled_at, created_at, modified_at, created_by, modified_by FROM i_rss_feeds ORDER BY modified_at DESC, id DESC',
|
||||
countSql: 'SELECT COUNT(*) AS count FROM i_rss_feeds',
|
||||
searchColumns: ['name', 'feed_url'],
|
||||
searchTerm: searchTerm,
|
||||
@@ -48,7 +48,7 @@ async function fetchRssFeedsPage(pool, page, pageSize, searchTerm, sortKey, sort
|
||||
|
||||
async function fetchRssFeedById(pool, id) {
|
||||
const [rows] = await pool.query(
|
||||
'SELECT id, name, feed_url, update_interval_value, update_interval_unit, item_limit, created_at, modified_at, created_by, modified_by FROM i_rss_feeds WHERE id = ?',
|
||||
'SELECT id, name, feed_url, enabled, update_interval_value, update_interval_unit, item_limit, last_pulled_at, created_at, modified_at, created_by, modified_by FROM i_rss_feeds WHERE id = ?',
|
||||
[id]
|
||||
);
|
||||
|
||||
|
||||
+16
-1
@@ -444,7 +444,22 @@ async function buildTemplateContent(pool, template, body, filesByField, existing
|
||||
font_size: style.font_size,
|
||||
font_color: style.font_color
|
||||
};
|
||||
} else if (!['text', 'image', 'video', 'webpage', 'qr-code', 'html', 'rtmp', 'rss', 'api'].includes(String(region.region_type || '').trim())) {
|
||||
} else if (region.region_type === 'weather') {
|
||||
const current = existingContent && existingContent[region.region_key] ? existingContent[region.region_key] : {};
|
||||
const locationId = body[`region_weather_location_id_${region.id}`];
|
||||
const submittedForecastMode = body[`region_weather_forecast_mode_${region.id}`];
|
||||
const forecastMode = ['current', 'hourly'].includes(submittedForecastMode) ? submittedForecastMode : 'daily';
|
||||
const style = getTextRegionStyle(body, region, existingContent);
|
||||
content[region.region_key] = {
|
||||
type: 'weather',
|
||||
value: body[`region_text_${region.id}`] !== undefined ? String(body[`region_text_${region.id}`] || '') : String(current.value || ''),
|
||||
weather_location_id: locationId === undefined || locationId === null ? (current.weather_location_id || null) : (locationId === '' ? null : Number(locationId)),
|
||||
forecast_mode: body[`region_weather_forecast_mode_${region.id}`] === undefined ? (['current', 'hourly'].includes(current.forecast_mode) ? current.forecast_mode : 'daily') : forecastMode,
|
||||
font_family: style.font_family,
|
||||
font_size: style.font_size,
|
||||
font_color: style.font_color
|
||||
};
|
||||
} else if (!['text', 'image', 'video', 'webpage', 'qr-code', 'html', 'rtmp', 'rss', 'api', 'weather'].includes(String(region.region_type || '').trim())) {
|
||||
const current = existingContent && existingContent[region.region_key] && typeof existingContent[region.region_key] === 'object' ? existingContent[region.region_key] : {};
|
||||
const suffix = '_' + region.id;
|
||||
const generic = {};
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
// Convert cached weather snapshots for display without refetching.
|
||||
|
||||
function convertTemperature(value, fromUnit, toUnit) {
|
||||
const number = Number(value);
|
||||
if (!Number.isFinite(number) || fromUnit === toUnit) return value;
|
||||
const converted = toUnit === 'fahrenheit' ? number * 9 / 5 + 32 : (number - 32) * 5 / 9;
|
||||
return Math.round(converted * 10) / 10;
|
||||
}
|
||||
|
||||
function convertWind(value, fromUnit, toUnit) {
|
||||
const number = Number(value);
|
||||
if (!Number.isFinite(number) || fromUnit === toUnit) return value;
|
||||
const metresPerSecond = fromUnit === 'mph' ? number * 0.44704 : fromUnit === 'kmh' ? number / 3.6 : number;
|
||||
const converted = toUnit === 'mph' ? metresPerSecond / 0.44704 : toUnit === 'kmh' ? metresPerSecond * 3.6 : metresPerSecond;
|
||||
return Math.round(converted * 10) / 10;
|
||||
}
|
||||
|
||||
function convertPrecipitation(value, fromUnit, toUnit) {
|
||||
const number = Number(value);
|
||||
if (!Number.isFinite(number) || fromUnit === toUnit) return value;
|
||||
const converted = toUnit === 'inch' ? number / 25.4 : number * 25.4;
|
||||
return Math.round(converted * 100) / 100;
|
||||
}
|
||||
|
||||
function temperatureUnitFromLabel(label) {
|
||||
return /f/i.test(String(label || '')) ? 'fahrenheit' : 'celsius';
|
||||
}
|
||||
|
||||
function windUnitFromLabel(label) {
|
||||
const value = String(label || '').toLowerCase();
|
||||
return value.includes('mph') ? 'mph' : value.includes('m/s') ? 'ms' : 'kmh';
|
||||
}
|
||||
|
||||
function precipitationUnitFromLabel(label) {
|
||||
return /in/i.test(String(label || '')) ? 'inch' : 'mm';
|
||||
}
|
||||
|
||||
function convertField(data, fields, converter, fromUnit, toUnit) {
|
||||
fields.forEach(function (field) {
|
||||
if (data[field] === undefined || data[field] === null) return;
|
||||
data[field] = Array.isArray(data[field])
|
||||
? data[field].map(function (value) { return converter(value, fromUnit, toUnit); })
|
||||
: converter(data[field], fromUnit, toUnit);
|
||||
});
|
||||
}
|
||||
|
||||
function convertWeatherSnapshot(snapshot, targetUnits) {
|
||||
const source = snapshot && typeof snapshot === 'object' ? snapshot : {};
|
||||
const target = Object.assign({ temperature: 'celsius', wind: 'kmh', precipitation: 'mm' }, targetUnits || {});
|
||||
const result = JSON.parse(JSON.stringify(source));
|
||||
const currentUnits = source.current_units || {};
|
||||
const hourlyUnits = source.hourly_units || currentUnits;
|
||||
const dailyUnits = source.daily_units || currentUnits;
|
||||
|
||||
convertField(result.current || {}, ['temperature_2m', 'apparent_temperature'], convertTemperature, temperatureUnitFromLabel(currentUnits.temperature_2m), target.temperature);
|
||||
convertField(result.current || {}, ['wind_speed_10m'], convertWind, windUnitFromLabel(currentUnits.wind_speed_10m), target.wind);
|
||||
convertField(result.current || {}, ['precipitation', 'rain'], convertPrecipitation, precipitationUnitFromLabel(currentUnits.precipitation), target.precipitation);
|
||||
convertField(result.hourly || {}, ['temperature_2m'], convertTemperature, temperatureUnitFromLabel(hourlyUnits.temperature_2m), target.temperature);
|
||||
convertField(result.hourly || {}, ['wind_speed_10m'], convertWind, windUnitFromLabel(hourlyUnits.wind_speed_10m), target.wind);
|
||||
convertField(result.hourly || {}, ['precipitation'], convertPrecipitation, precipitationUnitFromLabel(hourlyUnits.precipitation), target.precipitation);
|
||||
convertField(result.daily || {}, ['temperature_2m_max', 'temperature_2m_min'], convertTemperature, temperatureUnitFromLabel(dailyUnits.temperature_2m_max), target.temperature);
|
||||
convertField(result.daily || {}, ['wind_speed_10m_max'], convertWind, windUnitFromLabel(dailyUnits.wind_speed_10m_max), target.wind);
|
||||
convertField(result.daily || {}, ['precipitation_sum'], convertPrecipitation, precipitationUnitFromLabel(dailyUnits.precipitation_sum), target.precipitation);
|
||||
return result;
|
||||
}
|
||||
|
||||
module.exports = { convertWeatherSnapshot };
|
||||
@@ -0,0 +1,138 @@
|
||||
// Weather location data access and form normalization.
|
||||
|
||||
const { fetchPagedRows, validateMaxLength } = require('./utils');
|
||||
const { fetchAppSettings } = require('./app-settings');
|
||||
|
||||
const NAME_MAX_LENGTH = 255;
|
||||
const LOCATION_MAX_LENGTH = 255;
|
||||
const TIMEZONE_MAX_LENGTH = 128;
|
||||
const PROVIDERS = ['open-meteo', 'pirate-weather'];
|
||||
const TEMPERATURE_UNITS = ['celsius', 'fahrenheit'];
|
||||
const WIND_UNITS = ['kmh', 'mph', 'ms'];
|
||||
const PRECIPITATION_UNITS = ['mm', 'inch'];
|
||||
|
||||
async function fetchWeatherLocationSuggestions(query) {
|
||||
const search = validateMaxLength(query, LOCATION_MAX_LENGTH, 'Location search');
|
||||
if (!search) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const response = await fetch('https://geocoding-api.open-meteo.com/v1/search?name=' + encodeURIComponent(search) + '&count=8&language=en&format=json', {
|
||||
headers: { Accept: 'application/json', 'User-Agent': 'Pulse Signage weather location lookup' }
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('Weather location lookup failed.');
|
||||
}
|
||||
const data = await response.json();
|
||||
return (Array.isArray(data.results) ? data.results : []).map(function (result) {
|
||||
return {
|
||||
label: [result.name, result.admin1, result.country].filter(Boolean).join(', '),
|
||||
latitude: Number(result.latitude),
|
||||
longitude: Number(result.longitude),
|
||||
timezone: String(result.timezone || '')
|
||||
};
|
||||
}).filter(function (result) {
|
||||
return result.label && Number.isFinite(result.latitude) && Number.isFinite(result.longitude) && result.timezone;
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeChoice(value, choices, fallback) {
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
return choices.includes(normalized) ? normalized : fallback;
|
||||
}
|
||||
|
||||
async function fetchWeatherLocationsPage(pool, page, pageSize, searchTerm, sortKey, sortDirection) {
|
||||
const paged = await fetchPagedRows(pool, {
|
||||
selectSql: 'SELECT id, name, location_label, latitude, longitude, timezone, provider, temperature_unit, wind_unit, precipitation_unit, enabled, update_interval_value, update_interval_unit, last_pulled_at, last_pull_error, created_at, modified_at FROM i_weather_locations ORDER BY modified_at DESC, id DESC',
|
||||
countSql: 'SELECT COUNT(*) AS count FROM i_weather_locations',
|
||||
searchColumns: ['name', 'location_label', 'timezone', 'provider'],
|
||||
searchTerm: searchTerm,
|
||||
sortColumns: {
|
||||
name: 'name',
|
||||
location: 'location_label',
|
||||
provider: 'provider',
|
||||
interval: ['update_interval_value', 'update_interval_unit'],
|
||||
last_pulled: 'last_pulled_at',
|
||||
modified: 'modified_at'
|
||||
},
|
||||
sortKey: sortKey,
|
||||
sortDirection: sortDirection,
|
||||
page: page,
|
||||
pageSize: pageSize
|
||||
});
|
||||
return Object.assign({ weatherLocations: paged.rows }, paged);
|
||||
}
|
||||
|
||||
async function fetchWeatherLocationsData(pool) {
|
||||
const [rows] = await pool.query('SELECT id, name, location_label, latitude, longitude, timezone, provider, temperature_unit, wind_unit, precipitation_unit, enabled, update_interval_value, update_interval_unit, last_pulled_at, last_pull_error, last_response_json, created_at, modified_at FROM i_weather_locations ORDER BY modified_at DESC, id DESC');
|
||||
return { weatherLocations: rows };
|
||||
}
|
||||
|
||||
async function fetchWeatherLocationById(pool, id) {
|
||||
const [rows] = await pool.query('SELECT id, name, location_label, latitude, longitude, timezone, provider, temperature_unit, wind_unit, precipitation_unit, enabled, update_interval_value, update_interval_unit, last_pulled_at, last_pull_error, last_response_json, created_at, modified_at, created_by, modified_by FROM i_weather_locations WHERE id = ?', [id]);
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
async function fetchWeatherLocationForecast(pool, location) {
|
||||
const source = location || {};
|
||||
const settings = await fetchAppSettings(pool);
|
||||
const latitude = Number(source.latitude);
|
||||
const longitude = Number(source.longitude);
|
||||
const temperatureUnit = source.temperature_unit === 'fahrenheit' ? 'fahrenheit' : 'celsius';
|
||||
const windUnit = source.wind_unit === 'mph' ? 'mph' : source.wind_unit === 'ms' ? 'ms' : 'kmh';
|
||||
const precipitationUnit = source.precipitation_unit === 'inch' ? 'inch' : 'mm';
|
||||
let url;
|
||||
let headers = { Accept: 'application/json', 'User-Agent': 'Pulse Signage weather reader' };
|
||||
|
||||
if (source.provider === 'pirate-weather') {
|
||||
const apiKey = String(settings['weather.pirate_weather_api_key'] || '').trim();
|
||||
if (!apiKey) throw new Error('Pirate Weather API key is not configured.');
|
||||
url = 'https://api.pirateweather.net/forecast/' + encodeURIComponent(apiKey) + '/' + latitude + ',' + longitude + '?units=' + (temperatureUnit === 'fahrenheit' ? 'us' : 'si');
|
||||
} else {
|
||||
const params = new URLSearchParams({ latitude: String(latitude), longitude: String(longitude), timezone: String(source.timezone || 'auto'), forecast_days: '7', current: 'temperature_2m,relative_humidity_2m,apparent_temperature,is_day,precipitation,rain,weather_code,wind_speed_10m,wind_direction_10m,uv_index,cloud_cover', hourly: 'temperature_2m,precipitation_probability,precipitation,weather_code,wind_speed_10m,uv_index,cloud_cover', daily: 'weather_code,temperature_2m_max,temperature_2m_min,sunrise,sunset,precipitation_probability_max,precipitation_sum,wind_speed_10m_max,uv_index_max,cloud_cover_mean', temperature_unit: temperatureUnit, wind_speed_unit: windUnit, precipitation_unit: precipitationUnit });
|
||||
const apiKey = String(settings['weather.open_meteo_api_key'] || '').trim();
|
||||
if (apiKey) params.set('apikey', apiKey);
|
||||
url = 'https://api.open-meteo.com/v1/forecast?' + params.toString();
|
||||
}
|
||||
|
||||
const response = await fetch(url, { headers: headers });
|
||||
if (!response.ok) throw new Error('Weather provider returned HTTP ' + response.status + '.');
|
||||
const snapshot = await response.json();
|
||||
return { snapshot: snapshot, responseJson: JSON.stringify(snapshot), fetchedAt: new Date() };
|
||||
}
|
||||
|
||||
function buildWeatherLocationPayload(req, existingLocation) {
|
||||
const body = req && req.body ? req.body : {};
|
||||
const fallback = existingLocation || {};
|
||||
const name = validateMaxLength(body.name || fallback.name || '', NAME_MAX_LENGTH, 'Weather location name');
|
||||
const locationLabel = validateMaxLength(body.location_label || fallback.location_label || '', LOCATION_MAX_LENGTH, 'Location label');
|
||||
const latitude = Number(body.latitude !== undefined ? body.latitude : fallback.latitude);
|
||||
const longitude = Number(body.longitude !== undefined ? body.longitude : fallback.longitude);
|
||||
const timezone = validateMaxLength(body.timezone || fallback.timezone || '', TIMEZONE_MAX_LENGTH, 'Timezone');
|
||||
const provider = normalizeChoice(body.provider || fallback.provider, PROVIDERS, 'open-meteo');
|
||||
const temperatureUnit = normalizeChoice(body.temperature_unit || fallback.temperature_unit, TEMPERATURE_UNITS, 'celsius');
|
||||
const windUnit = normalizeChoice(body.wind_unit || fallback.wind_unit, WIND_UNITS, 'kmh');
|
||||
const precipitationUnit = normalizeChoice(body.precipitation_unit || fallback.precipitation_unit, PRECIPITATION_UNITS, 'mm');
|
||||
const updateIntervalValue = Number(body.update_interval_value || fallback.update_interval_value || 30);
|
||||
const updateIntervalUnit = normalizeChoice(body.update_interval_unit || fallback.update_interval_unit, ['minutes', 'hours'], 'minutes');
|
||||
|
||||
if (!name || !locationLabel || !timezone) {
|
||||
const error = new Error('Name, location label, and timezone are required.');
|
||||
error.statusCode = 400;
|
||||
throw error;
|
||||
}
|
||||
if (!Number.isFinite(latitude) || latitude < -90 || latitude > 90 || !Number.isFinite(longitude) || longitude < -180 || longitude > 180) {
|
||||
const error = new Error('Latitude must be between -90 and 90, and longitude must be between -180 and 180.');
|
||||
error.statusCode = 400;
|
||||
throw error;
|
||||
}
|
||||
if (!Number.isInteger(updateIntervalValue) || updateIntervalValue < 1 || updateIntervalValue > 1440) {
|
||||
const error = new Error('Update interval must be a whole number between 1 and 1440.');
|
||||
error.statusCode = 400;
|
||||
throw error;
|
||||
}
|
||||
|
||||
return { name, locationLabel, latitude, longitude, timezone, provider, temperatureUnit, windUnit, precipitationUnit, updateIntervalValue, updateIntervalUnit };
|
||||
}
|
||||
|
||||
module.exports = { fetchWeatherLocationsData, fetchWeatherLocationsPage, fetchWeatherLocationById, fetchWeatherLocationSuggestions, fetchWeatherLocationForecast, buildWeatherLocationPayload, PROVIDERS, TEMPERATURE_UNITS, WIND_UNITS, PRECIPITATION_UNITS };
|
||||
Reference in New Issue
Block a user