"use strict"; import { getDatastreamIdFromBuildingNumber } from "./getDatastreamId.mjs"; /** * Create URL to fetch the details of single Datastream * @param {String} baseUrl Base URL of the STA server * @param {Number} datastreamID Integer representing the Datastream ID * @returns {String} URL string for fetching a single Datastream */ const createDatastreamUrl = function (baseUrl, datastreamID) { if (!datastreamID) return; return `${baseUrl}/Datastreams(${datastreamID})`; }; /** * Create URL to fetch Observations * @param {String} baseUrl Base URL of the STA server * @param {Number} datastreamID Integer representing the Datastream ID * @returns {String} URL string for fetching Observations */ const createObservationsUrl = function (baseUrl, datastreamID) { if (!datastreamID) return; return `${baseUrl}/Datastreams(${datastreamID})/Observations`; }; /** * Perform a GET request to fetch a single Datastream using the Axios library * * @param {String} urlDatastream A URL that fetches a single Datastream from an STA instance * @returns {Promise} A promise that contains the Datastream metadata when fulfilled */ const getDatastream = function (urlDatastream) { return axios.get(urlDatastream); }; /** * Perform a GET request using the Axios library * @param {String} urlObservations A URL that fetches Observations from an STA instance * @param {Object} urlParamObj The URL parameters to be sent together with the GET request * @returns {Promise} A promise that contains the first page of results when fulfilled */ const getObservations = function (urlObservations, urlParamObj) { return axios.get(urlObservations, { params: urlParamObj, }); }; /** * Extract the metadata from a single datastream * @async * @param {String} urlDatastream A URL that fetches a Datastream from an STA instance * @returns {Promise} A promise that contains a metadata object for a Datastream when fulfilled */ const extractMetadataFromSingleDatastream = async function (urlDatastream) { try { // Extract properties of interest const { data: { description, name, unitOfMeasurement }, } = await getDatastream(urlDatastream); return { description, name, unitOfMeasurement }; } catch (err) { // Server responded with status code outside of 2xx range if (err.response) { throw new Error( `The request to fetch Datastream metadata was made but the server responded with: \n${err.message}` ); } // Request was made but no response was received else if (err.request) { throw new Error( `The request to fetch Datastream metadata was made but no response was received: \n${err.message}` ); } // Problem with setting up the request else { throw new Error( `There was a problem setting up the request to fetch Datastream metadata: \n${err.message}` ); } } }; /** * Extract the metadata from multiple datastreams * @async * @param {Array} datastreamsUrlArr An array that contains N Datastream URL strings * @returns {Promise} A promise that contains an array of N Datastream metadata objects when fulfilled */ const extractMetadataFromMultipleDatastreams = async function ( datastreamsUrlArr ) { try { // Array to store our final result const datastreamMetadataArr = []; // Use for/of loop - we need to maintain the order of execution of the async operations for (const datastreamUrl of datastreamsUrlArr) { // Metadata from a single Datastream const datastreamMetadata = await extractMetadataFromSingleDatastream( datastreamUrl ); datastreamMetadataArr.push(datastreamMetadata); } return datastreamMetadataArr; } catch (err) { console.error(err); } }; /** * Traverses all the pages that make up the response from a SensorThingsAPI instance. The link to the next page, if present, is denoted by the presence of a "@iot.nextLink" property in the response object. This function concatenates all the values so that the complete results are returned in one array. * @async * @param {Promise} httpGetRequestPromise Promise object resulting from an Axios GET request * @returns {Promise} A promise that contains an object containing results from all the pages when fulfilled */ const combineResultsFromAllPages = async function (httpGetRequestPromise) { try { if (!httpGetRequestPromise) return; const lastSuccess = await httpGetRequestPromise; // The "success" objects contain a "data" object which in turn has a "value" property // If the "data" object in turn has a "@iot.nextLink" property, then a next page exists if (lastSuccess.data["@iot.nextLink"]) { const nextLinkSuccess = await combineResultsFromAllPages( axios.get(lastSuccess.data["@iot.nextLink"]) ); // The "data" object in turn has a "value" property // The "value" property's value is an array nextLinkSuccess.data.value = lastSuccess.data.value.concat( nextLinkSuccess.data.value ); return nextLinkSuccess; } else { return lastSuccess; } } catch (err) { // Server responded with status code outside of 2xx range if (err.response) { throw new Error( `The request to fetch Observations was made but the server responded with: \n${err.message}` ); } // Request was made but no response was received else if (err.request) { throw new Error( `The request to fetch Observations was made but no response was received: \n${err.message}` ); } // Problem with setting up the request else { throw new Error( `There was a problem setting up the request to fetch Observations: \n${err.message}` ); } } }; /** * Traverses all the pages that make up the response from a SensorThingsAPI instance and extracts the combined Observations * @async * @param {Promise} httpGetRequestPromise Promise object resulting from an Axios GET request * @returns {Promise} A promise that contains an array of Observations when fulfilled */ const extractCombinedObservationsFromAllPages = async function ( httpGetRequestPromise ) { try { const successResponse = await combineResultsFromAllPages( httpGetRequestPromise ); // Extract value array from the success response object const { data, data: { value: valueArr }, } = successResponse; // Array that will hold the combined observations const combinedObservations = []; valueArr.forEach((val) => { // Each page of results will have a dataArray that holds the observations const { dataArray } = val; combinedObservations.push(...dataArray); }); return new Promise((resolve, reject) => { resolve(combinedObservations); }); } catch (err) { console.error(err); } }; /** * Retrieve all the Observations from an array of Observations promises * @async * @param {Promise} observationPromiseArray An array that contains N observation promises * @returns {Promise} A promise that contains an array of Observations from multiple Datastreams when fulfilled */ const getObservationsFromMultipleDatastreams = async function ( observationPromiseArray ) { try { // Array to store our final result const observationsAllDatastreamsArr = []; // Use for/of loop - we need to maintain the order of execution of the async operations for (const observationPromise of observationPromiseArray) { // Observations from a single Datastream const observations = await observationPromise; observationsAllDatastreamsArr.push(observations); } return observationsAllDatastreamsArr; } catch (err) { console.error(err); } }; /** * Retrieve the metadata from a single Datastream or multiple Datastreams and the Observations corresponding to the Datastream(s) * @async * @param {String} baseUrl Base URL of the STA server * @param {Object} urlParamObj The URL parameters to be sent together with the GET request * @param {Array} bldgSensorSamplingRateNestedArr A N*1 array (where N >= 1) containing a nested array of buildings, sensors & sampling rates as strings, i.e. [["101", "rl", "15min"]] or [["101", "rl", "15min"], ["102", "vl", "60min"]] or [["101", "rl", "15min"], ["102", "vl", "60min"], ["225", "vl", "60min"]], etc * @returns {Promise} A promise that contains a 1*2 array (the first element is an array that contans N Observations arrays; and the second element is an array of N Datastream metadata objects) when fulfilled */ const getMetadataPlusObservationsFromSingleOrMultipleDatastreams = async function (baseUrl, urlParamObj, bldgSensorSamplingRateNestedArr) { try { if (!bldgSensorSamplingRateNestedArr) return; // Datastreams IDs const datastreamsIdsArr = bldgSensorSamplingRateNestedArr.map( (bldgSensorSamplingRateArr) => getDatastreamIdFromBuildingNumber(...bldgSensorSamplingRateArr) ); // Observations URLs const observationsUrlArr = datastreamsIdsArr.map((datastreamId) => createObservationsUrl(baseUrl, datastreamId) ); // Datastreams URLs const datastreamsUrlArr = datastreamsIdsArr.map((datastreamId) => createDatastreamUrl(baseUrl, datastreamId) ); // Promise objects - Observations const observationsPromisesArr = observationsUrlArr.map((obsUrl) => extractCombinedObservationsFromAllPages( getObservations(obsUrl, urlParamObj) ) ); // Observations array const observationsArr = await getObservationsFromMultipleDatastreams( observationsPromisesArr ); // Metadata array const metadataArr = await extractMetadataFromMultipleDatastreams( datastreamsUrlArr ); return [observationsArr, metadataArr]; } catch (err) { console.error(err); } }; /** * Check whether the raw observations and metadata have been successfully fetched, otherwise throw an error * * @param {Array} observationsRawPlusMetadataArr A 1*2 array (the first element is an array that contans N Observations arrays; and the second element is an array of N Datastream metadata objects) * @returns {Boolean} true, if the raw metadata and observations are successfully retrieved, otherwise an error is thrown */ const isFetchingRawMetadataPlusObservationsSuccessful = function ( observationsRawPlusMetadataArr ) { // If there is an error in fetching metadata + observations (raw observations) // the returned array will have this structure: [[undefined, undefined], undefined] // Note that the second element is not an array as we would expect but is a // a single `undefined` value if (typeof observationsRawPlusMetadataArr[0][0] === "undefined") { throw new Error( `There was a problem in fetching metadata and observations` ); } // If metadata + observations fetched successfully else { return true; } }; export { getMetadataPlusObservationsFromSingleOrMultipleDatastreams, isFetchingRawMetadataPlusObservationsSuccessful, };