diff --git a/src/index.js b/src/index.js index 1053b32e59e01e1228fd53bb0008b7ff258a0614..bbc9e0d3923462a6f4f4203724d13965b1a4d71c 100644 --- a/src/index.js +++ b/src/index.js @@ -3,8 +3,8 @@ const { log, errors, addData, + updateOrCreate, hydrateAndFilter, - cozyClient, } = require('cozy-konnector-libs') const axios = require('axios').default @@ -20,8 +20,7 @@ const Tracing = require('@sentry/tracing') // Needed for tracking performance in const { version } = require('../package.json') const { isDev } = require('./helpers/env') -const manualExecution = - process.env.COZY_JOB_MANUAL_EXECUTION === 'true' ? true : false +const manualExecution = process.env.COZY_JOB_MANUAL_EXECUTION === 'true' const startDate = manualExecution ? moment() @@ -96,13 +95,32 @@ async function start(fields, cozyParameters) { const eglData = await getData(response, baseUrl, apiAuthKey) if (eglData) { log('debug', 'Process egl daily data') - const processedLoadData = await processData( + const filteredDocuments = await hydrateAndFilter( eglData, rangeDate.day.doctype, - rangeDate.day.keys + { + keys: rangeDate.day.keys, + } + ) + await addData(filteredDocuments, rangeDate.day.doctype) + + log('debug', 'Aggregate egl monthly load data') + const monthlyLoads = aggregateMonthlyLoad(eglData) + log('debug', 'Store aggregated egl monthly load data') + await updateOrCreate( + monthlyLoads, + rangeDate.month.doctype, + rangeDate.month.keys + ) + + log('debug', 'Aggregate egl yearly load data') + const yearlyLoads = aggregateYearlyLoad(monthlyLoads) + log('debug', 'Store aggregated egl yearly load data') + await updateOrCreate( + yearlyLoads, + rangeDate.year.doctype, + rangeDate.year.keys ) - log('debug', 'Aggregate egl load data for month and year') - await aggregateMonthAndYearData(processedLoadData) } else { log('debug', 'No data found') } @@ -124,83 +142,78 @@ async function start(fields, cozyParameters) { } } -/** - * Parse data - * Remove existing data from DB using hydrateAndFilter - * Store filtered data - * Return the list of filtered data - */ -async function processData(data, doctype, filterKeys) { - log('debug', 'processData - data formatted') - // Remove data for existing days into the DB - const filteredData = await hydrateAndFilter(data, doctype, { - keys: filterKeys, - }) - log('debug', 'processData - data filtered') - // Store new day data - await storeData(filteredData, doctype, filterKeys) - return filteredData -} +function aggregateMonthlyLoad(data) { + const monthlyLoad = {} -/** - * Aggregate data from daily data to monthly and yearly data - */ -async function aggregateMonthAndYearData(data) { - // Sum year and month values into object with year or year-month as keys - if (data && data.length !== 0) { - let monthData = {} - let yearData = {} - - data.forEach(element => { - const monthDataKey = element.year + '-' + element.month - if (monthDataKey in monthData) { - monthData[monthDataKey] += element.load - } else { - monthData[monthDataKey] = element.load + for (const entry of data) { + const { year, month, load } = entry + const monthKey = `${year}-${month}` + + if (!monthlyLoad[monthKey]) { + monthlyLoad[monthKey] = { + load: load, + year: year, + month: month, + day: 0, + hour: 0, + minute: 0, + type: 'R', } + } else { + monthlyLoad[monthKey].load += load + } + } + + return Object.values(monthlyLoad) +} - const yearDataKey = element.year - if (yearDataKey in yearData) { - yearData[yearDataKey] += element.load - } else { - yearData[yearDataKey] = element.load +function aggregateYearlyLoad(data) { + const yearlyLoad = {} + + for (const entry of data) { + const { year, load } = entry + if (!yearlyLoad[year]) { + yearlyLoad[year] = { + load: load, + year: year, + month: 0, + day: 0, + hour: 0, + minute: 0, + type: 'R', } - }) - // Aggregation for Month data - const aggregatedMonthData = await buildAggregatedData( - monthData, - 'com.grandlyon.egl.month' - ) - await storeData(aggregatedMonthData, 'com.grandlyon.egl.month', [ - 'year', - 'month', - ]) - // Aggregation for Year data - const aggregatedYearData = await buildAggregatedData( - yearData, - 'com.grandlyon.egl.year' - ) - await storeData(aggregatedYearData, 'com.grandlyon.egl.year', ['year']) + } else { + yearlyLoad[year].load += load + } } + + return Object.values(yearlyLoad) } /** - * Retrieve and remove old data for a specific doctype - * Return an Array of aggregated data + * @typedef {Object} AuthResponse + * @property {number} codeRetour + * @property {string} libelleRetour + * @property {AuthResult} resultatRetour + */ + +/** + * @typedef {Object} AuthResult + * @property {number} num_abt + * @property {string} token */ -async function buildAggregatedData(data, doctype) { - log('info', 'entering buildAggregatedData') - let aggregatedData = [] - for (let [key, value] of Object.entries(data)) { - const data = await buildDataFromKey(doctype, key, value) - const oldValue = await resetInProgressAggregatedData(data, doctype) - log('info', 'Data load + old value is ' + data.load + ' + ' + oldValue) - data.load += oldValue - aggregatedData.push(data) - } - return aggregatedData -} +/** + * Authenticates a user with the provided credentials and returns an authentication response. + * + * @param {number} login - The user's login. + * @param {string} password - The user's password. + * @param {string} baseUrl - The base URL for the authentication request. + * @param {string} apiAuthKey - The API authentication key. + * + * @throws {Error} - Throws a Cozy error (VENDOR_DOWN or LOGIN_FAILED) in case of authentication failure. + * @returns {Promise<AuthResponse>} - The authentication response containing a token. + */ async function authenticate(login, password, baseUrl, apiAuthKey) { log('info', 'Authenticating ...') const authRequest = { @@ -217,11 +230,13 @@ async function authenticate(login, password, baseUrl, apiAuthKey) { } try { - const resp = await axios(authRequest) - if (resp.data.codeRetour === 100) { - return resp.data + /** @type {AuthResponse} */ + const respData = (await axios(authRequest)).data + + if (respData.codeRetour === 100) { + return respData } - const errorMessage = `Authentication failed. Response data: ${resp?.data?.libelleRetour}` + const errorMessage = `Authentication failed. Response data: ${respData.libelleRetour}` log('error', errorMessage) throw new Error(errors.VENDOR_DOWN) } catch (error) { @@ -241,6 +256,30 @@ async function authenticate(login, password, baseUrl, apiAuthKey) { } } +/** + * @typedef {Object} GetDataResponse + * @property {number} codeRetour + * @property {string} libelleRetour + * @property {Array<Releve>} resultatRetour + */ + +/** + * @typedef {Object} Releve + * @property {string} DateReleve + * @property {string} TypeAgregat + * @property {number} ValeurIndex + */ + +/** + * Retrieves data from a specified API using the provided response data and API configuration. + * + * @param {ApiResponse} response - The authentication response containing a valid token. + * @param {string} baseUrl - The base URL for the data request. + * @param {string} apiAuthKey - The API authentication key. + * + * @throws {Error} - Throws an error with an error code in case of data retrieval failure. + * @returns {Promise<FormattedData>} - A promise that resolves to the retrieved and formatted data. + */ async function getData(response, baseUrl, apiAuthKey) { const dataRequest = { method: 'post', @@ -256,32 +295,34 @@ async function getData(response, baseUrl, apiAuthKey) { date_fin: endDate, }, } + try { + /** @type {GetDataResponse} */ + const respData = (await axios(dataRequest)).data + // Sort data by date - const resp = await axios(dataRequest) - resp.data.resultatRetour.sort(function(a, b) { + respData.resultatRetour.sort(function(a, b) { return new Date(a.DateReleve) - new Date(b.DateReleve) }) - - switch (resp.data.codeRetour) { + switch (respData.codeRetour) { case 100: - return format(resp.data) + return format(respData) case -2: log( 'error', - `Get data failed. codeRetour -2. ${resp.data.libelleRetour}` + `Get data failed. codeRetour -2. ${respData.libelleRetour}` ) throw errors.LOGIN_FAILED case -1: log( 'error', - `Get data failed. codeRetour -1. ${resp.data.libelleRetour}` + `Get data failed. codeRetour -1. ${respData.libelleRetour}` ) throw errors.VENDOR_DOWN default: log( 'error', - `Get data failed. ${resp.data.codeRetour}. ${resp.data.libelleRetour}` + `Get data failed. ${respData.codeRetour}. ${respData.libelleRetour}` ) throw errors.UNKNOWN_ERROR } @@ -303,6 +344,27 @@ async function getData(response, baseUrl, apiAuthKey) { } } +/** + * Represents a formatted data object after processing the getData response. + * + * @typedef {Object} FormattedData + * @property {number} load - The processed load value. + * @property {number} year - The year of the data point. + * @property {number} month - The month of the data point. + * @property {number} day - The day of the data point. + * @property {number} hour - The hour of the data point (in this case, always 0). + * @property {number} minute - The minute of the data point (in this case, always 0). + * @property {string} type - The type of the data point. + */ + +/** + * Formats and processes data retrieved from the getData response. + * + * @param {GetDataResponse} response - The data response to be formatted. + * + * @throws {Error} - Throws an error with an error code in case of data processing failure. + * @returns {Array<FormattedData>} - An array of formatted data objects. + */ function format(response) { log('info', 'origin response size is: ' + response.resultatRetour.length) // Store first value as reference for index processing @@ -353,98 +415,3 @@ function format(response) { throw error } } - -/** - * Save data in the right doctype db and prevent duplicated keys - */ -async function storeData(data, doctype, filterKeys) { - log('debug', 'Store into ' + doctype) - log('debug', 'Store into keys : ' + filterKeys) - const filteredDocuments = await hydrateAndFilter(data, doctype, { - keys: filterKeys, - }) - return await addData(filteredDocuments, doctype) -} - -/** - * Format an entry for DB storage - * using key and value - * For year doctype: key = 'YYYY' - * For month doctype: key = 'YYYY-MM' - */ -async function buildDataFromKey(doctype, key, value) { - let year, month, day, hour - if (doctype === 'com.grandlyon.egl.year') { - year = key - month = 1 - day = 0 - hour = 0 - } else if (doctype === 'com.grandlyon.egl.month') { - const split = key.split('-') - year = split[0] - month = split[1] - day = 0 - hour = 0 - } else { - const split = key.split('-') - year = split[0] - month = split[1] - day = split[2] - hour = split[3] - } - return { - load: Math.round(value * 10000) / 10000, - year: parseInt(year), - month: parseInt(month), - day: parseInt(day), - hour: parseInt(hour), - minute: 0, - } -} - -/** - * Function handling special case. - * The temporary aggregated data need to be remove in order for the most recent one te be saved. - * ex for com.grandlyon.egl.month : - * { load: 76.712, month: 2020, ... } need to be replace by - * { load: 82.212, month: 2020, ... } after egl data reprocess - */ -async function resetInProgressAggregatedData(data, doctype) { - // /!\ Warning: cannot use mongo queries because not supported for dev by cozy-konnectors-libs - log('debug', 'Remove aggregated data for ' + doctype) - const result = await cozyClient.data.findAll(doctype) - if (result && result.length > 0) { - // Filter data to remove - let filtered = [] - if (doctype === 'com.grandlyon.egl.year') { - // Yearly case - filtered = result.filter(function(el) { - return el.year == data.year - }) - } else if (doctype === 'com.grandlyon.egl.month') { - // Monthly case - filtered = result.filter(function(el) { - return el.year == data.year && el.month == data.month - }) - } else { - // Hourly case - filtered = result.filter(function(el) { - return ( - el.year == data.year && - el.month == data.month && - el.day == data.day && - el.hour == data.hour - ) - }) - } - // Remove data - let sum = 0.0 - for (const doc of filtered) { - sum += doc.load - log('debug', 'Removing this entry for ' + doc.load) - await cozyClient.data.delete(doctype, doc) - } - return sum - } - return 0.0 -}