/**
* Created by eliwinkelman on 9/20/19.
*/
const { DatabaseInterface } = require("../db") ;
const DeviceDatabase = require("./device");
const { strMapToObj } = require("../../utils/utils");
/**
* Manages the database storage for DeviceData objects.
* @class
* @implements DatabaseInterface
*/
class DeviceDataDatabase extends DatabaseInterface {
/**
* Helper function to get the collection of devices.
* @param {string} device_id - the id of the device whose data is being accessed.
* @returns {Object} The MongoDB collection for devices
*/
static async getCollection(device_id) {
const collection = await super.getCollection(device_id.toString());
return collection;
}
/**
* Determines if user owns the DeviceData with id
* @param {string} device_id - The id of the device to check for ownership.
* @param {Object} user - The user to check for ownership.
* @returns {boolean} True if the user does own the device, false otherwise.
*/
static async owns(device_id, user) {
return DeviceDatabase.owns(device_id, user)
}
/**
* Retrieves all device data for a device.
* @param {string} device_id - The id of the device.
* @returns {Object} The data belonging to the given device.
*/
static async getByDevice(device_id) {
const DeviceData = await this.getCollection(device_id);
const deviceData = await DeviceData.find({device_id: device_id}).toArray().catch((err) => {
throw err;
});
return this.__formatDeviceData(deviceData);
}
/**
* Retrieves all device data for a device for a specific data_run.
* @param {string} device_id - The id of the device.
* @param {number} data_run - The data_run to obtain
* @returns {Object} The data belonging to the given device for the data_run.
*/
static async getByDeviceDataRun(device_id, data_run) {
// Device data collections are named by their device ID.
const DeviceData = await this.getCollection(device_id);
const deviceData = await DeviceData.find({device_id: device_id, data_run: data_run}).toArray().catch((err) => {
throw err;
});
console.log("All data for data_run", data_run, ":", deviceData);
return this.__formatDeviceData(deviceData);
}
/**
* Creates a new device data object.
* @param {string} device_id - The id of the device the data belongs to.
* @param {Object} data - The data being reported by the device.
* @returns {Array}
*/
static async create(device_id, data) {
// Obtain the document in the device database which corresponds to the given device_id
const object = await DeviceDatabase.get(device_id);
// Create a dataRun object with the same number of dataRuns as the device of interest
let tempDataRun = new DataRun(object.num_dataRuns);
// Device data collections are named by their device ID.
const DeviceData = await this.getCollection(device_id);
// Determine and set the data_run (number) for the deviceData object (data)
await tempDataRun.getDataRun(data.data.contents, device_id);
data.data_run = tempDataRun.dataRun;
// Update the number of data runs for the device
await DeviceDatabase.update(device_id, {$set: {num_dataRuns: tempDataRun.num_dataRuns}});
// Insert the data object into the collection for the device
const insertedData = await DeviceData.insertOne(data).catch(err => {throw err;});
return insertedData;
}
static async getByDataRun(device_id, data_run, most_recent=false) {
const DeviceData = await this.getCollection(device_id);
if (most_recent) {
// Get the most recent deviceData document with the corresponding dataRun
const deviceData = await DeviceData.findOne({data_run: data_run}, {sort:{$natural:-1}}).catch((err) => {
throw err;
});
return deviceData
}
else {
const deviceData = await DeviceData.findOne({data_run: data_run}).catch((err) => {
throw err;
});
return deviceData
}
}
/**
* Formats arrays of device data to be more easily accessible
*
* @param {Array} deviceData - An array of raw devicedata from the database.
* @returns {Array} Reformated device data.
* @private
*/
static __formatDeviceData(deviceData) {
return deviceData.map((data, index) => {
let formatted_device = new Map();
formatted_device.set("Data_Run", data.data_run);
formatted_device.set("Date", data.data.timestamp.date);
formatted_device.set("Time", data.data.timestamp.time);
// Sensor refers to an object in the "contents" array. For every key
// in each sensor, concatenate the name of the sensor with the key
// for the data label and set the data to that label.
data.data.contents.forEach((sensor) => {
for (var key in sensor.data) {
if (sensor.data.hasOwnProperty(key)) {
formatted_device.set(String(sensor.module) + '-' + String(key), sensor.data[key]);
}
}
});
return strMapToObj(formatted_device);
});
};
}
/**
* Defines a class to track the configuration of sensors for Devices.
*
* @class DataRun
*/
class DataRun {
// Set default dataRun to 1 and num_dataRuns to 0
dataRun = 1;
num_dataRuns = 0;
constructor(num_dataRuns) {
if (num_dataRuns !== null && num_dataRuns !== undefined){
this.num_dataRuns = num_dataRuns;
}
else {
this.num_dataRuns = 0;
}
}
/**
* Determines the data_run for a new deviceData object based on the modules
* in its contents array.
*
* 1. For the new deviceData object, fill an array of strings with each
* module-key pair.
* 2. For each prior data_run, use one document to fill an array of strings
* with each module-key pair.
* 3. Compare each index of the array. If they match, set the data_run.
* 4. If the new deviceData object is distinct from all past data_runs,
* increment the number of data_runs for the Device and set the data_run
* for the new object to the new maximum.
*
* @param {string} newSchema - The data contents array of the deviceData
* object which contains modules and key-value pairs for data points.
* @param {string} device_id - The id of the device
* @returns {DataRun} The DataRun object which called the function,
* containing the dataRun for the deviceData object and the total number of
* data runs for the device.
*/
async getDataRun(newSchema, device_id) {
// This will track if there are any data entries yet. If not, data_run = 1
let firstEntry = true;
// Compare newSchema with schema from every past dataRun
let i;
for (i = 1; i <= this.num_dataRuns; i++) {
// Get a schema of module-key pairs from data_run i
const oldSchema = await DataRun.getSchema(i, device_id);
// If there is no schema for data_run i, check the next
if (oldSchema === null || oldSchema === undefined) {
continue;
}
else {
// If any oldSchema exists, this is not the first entry
firstEntry = false;
}
// First check if the schemas have the same number of sensors
if (newSchema.length !== oldSchema.length) {
continue;
}
else {
const sameSchema = await DataRun.compareModules(newSchema, oldSchema);
// If the new schema matches one prior, set the dataRun and return it
if (sameSchema === true) {
this.dataRun = i;
return this;
}
// Otherwise, check the next oldSchema
}
}
// If this is the first entry, don't change the dataRun
if (firstEntry) {
this.num_dataRuns = 1;
return this;
}
else {
// newSchema is different from all past schemas, so assign unique dataRun
this.num_dataRuns = this.num_dataRuns + 1;
this.dataRun = this.num_dataRuns;
return this;
}
}
/**
* Compares the schemas of two separate deviceData objects.
*
* 1. Fills an array of strings for each module-key pair
* 2. Compares the arrays for equality.
*
* @param {Array} newSchema - The data contents array of the deviceData
* object which contains modules and key-value pairs for data points.
* @param {Array} oldSchema - The data contents array of an old deviceData
* object which contains modules and key-value pairs for data points.
* @returns {{boolean}} - True if they have the same schemas (every
* module-key pair is identical)
*/
static async compareModules (newSchema, oldSchema) {
let j;
// Check every sensor in the arrays
for (j = 0; j < newSchema.length; j++) {
let newArray = await DataRun.modulekey_Array(j, newSchema);
let oldArray = await DataRun.modulekey_Array(j, oldSchema);
// If schemas don't have same number of keys, they are not identical
if (newArray.length !== oldArray.length) {
return false
}
// Check every data key for every module
let k;
for (k = 0; k < newArray.length; k++) {
const newSchemaSensor = newArray[k];
const oldSchemaSensor = oldArray[k];
if (newSchemaSensor !== oldSchemaSensor) {
return false; //need to go to the next data run
}
// If they are equal, check the next module-key pair
}
}
return true;
}
/**
* Declares and fills an array with strings for each module-key pair
* for a single module.
*
* 1. Counts the number of module-key pairs.
* 2. Initializes and fills the array with the module-key pairs.
*
* @param {number} j - The index corresponding to the module within
* the schema.
* @param {Array} schema - The data contents array of a deviceData
* object which contains modules and key-value pairs for data points.
* @returns {{Array}} - Returns the array of strings
*/
static async modulekey_Array(j, schema) {
let modulekey_array = [];
// Fill array with strings for each module-key pair
for (let key in schema[j].data) {
if (schema[j].data.hasOwnProperty(key)) {
modulekey_array.push(schema[j].module.toString() + '-' + key.toString());
}
}
return modulekey_array;
}
/**
* Declares and fills an array with strings for each module-key pair
* for a single module.
*
* 1. Counts the number of module-key pairs.
* 2. Initializes and fills the array with the module-key pairs.
*
* @param {number} dataRun - The index corresponding to the module within
* the schema.
* @param {string} device_id - The id of the device.
* @returns {{Array}} - Returns the data contents array for a single
* document from the given dataRun.
*/
static async getSchema(dataRun, device_id) {
const deviceData = await DeviceDataDatabase.getByDataRun(device_id, dataRun, true);
// If there are no deviceData documents for the given dataRun, return null
if (deviceData === null) {
return deviceData;
}
// Return the contents array of module/key objects for each sensor
return deviceData.data.contents;
}
}
module.exports = DeviceDataDatabase;