diff --git a/database/addAsset.d.ts b/database/addAsset.d.ts index 5580c0f..78f8c3f 100644 --- a/database/addAsset.d.ts +++ b/database/addAsset.d.ts @@ -1,3 +1,3 @@ import sqlite from 'better-sqlite3'; import type { Asset } from '../types/recordTypes.js'; -export declare function addAsset(asset: Partial, sessionUser: EmileUser, connectedEmileDB?: sqlite.Database): number; +export declare function addAsset(asset: Partial, sessionUser: EmileUser, connectedEmileDB?: sqlite.Database): Promise; diff --git a/database/addAsset.js b/database/addAsset.js index a93b9bb..6fecdab 100644 --- a/database/addAsset.js +++ b/database/addAsset.js @@ -1,6 +1,7 @@ import sqlite from 'better-sqlite3'; import { databasePath } from '../helpers/functions.database.js'; -export function addAsset(asset, sessionUser, connectedEmileDB) { +import { ensureEnergyDataTableExists } from './manageEnergyDataTables.js'; +export async function addAsset(asset, sessionUser, connectedEmileDB) { const emileDB = connectedEmileDB === undefined ? sqlite(databasePath) : connectedEmileDB; const rightNowMillis = Date.now(); const result = emileDB @@ -11,8 +12,10 @@ export function addAsset(asset, sessionUser, connectedEmileDB) { recordUpdate_userName, recordUpdate_timeMillis) values (?, ?, ?, ?, ?, ?, ?, ?)`) .run(asset.assetName, asset.categoryId, (asset.latitude ?? '') === '' ? undefined : asset.latitude, (asset.longitude ?? '') === '' ? undefined : asset.longitude, sessionUser.userName, rightNowMillis, sessionUser.userName, rightNowMillis); + const assetId = result.lastInsertRowid; + await ensureEnergyDataTableExists(assetId); if (connectedEmileDB === undefined) { emileDB.close(); } - return result.lastInsertRowid; + return assetId; } diff --git a/database/addAsset.ts b/database/addAsset.ts index 867d79d..1c3d5e0 100644 --- a/database/addAsset.ts +++ b/database/addAsset.ts @@ -3,11 +3,13 @@ import sqlite from 'better-sqlite3' import { databasePath } from '../helpers/functions.database.js' import type { Asset } from '../types/recordTypes.js' -export function addAsset( +import { ensureEnergyDataTableExists } from './manageEnergyDataTables.js' + +export async function addAsset( asset: Partial, sessionUser: EmileUser, connectedEmileDB?: sqlite.Database -): number { +): Promise { const emileDB = connectedEmileDB === undefined ? sqlite(databasePath) : connectedEmileDB @@ -33,9 +35,13 @@ export function addAsset( rightNowMillis ) + const assetId = result.lastInsertRowid as number + + await ensureEnergyDataTableExists(assetId) + if (connectedEmileDB === undefined) { emileDB.close() } - return result.lastInsertRowid as number + return assetId } diff --git a/database/addEnergyData.js b/database/addEnergyData.js index 21a1b8b..54106cb 100644 --- a/database/addEnergyData.js +++ b/database/addEnergyData.js @@ -1,6 +1,7 @@ import Debug from 'debug'; import { getConnectionWhenAvailable } from '../helpers/functions.database.js'; import { delay } from '../helpers/functions.utilities.js'; +import { ensureEnergyDataTableExists } from './manageEnergyDataTables.js'; const debug = Debug('emile:database:addEnergyData'); export async function addEnergyData(data, sessionUser, connectedEmileDB) { const emileDB = connectedEmileDB === undefined @@ -10,8 +11,9 @@ export async function addEnergyData(data, sessionUser, connectedEmileDB) { while (true) { try { const rightNowMillis = Date.now(); + const tableName = await ensureEnergyDataTableExists(data.assetId); result = emileDB - .prepare(`insert into EnergyData ( + .prepare(`insert into ${tableName} ( assetId, dataTypeId, fileId, timeSeconds, durationSeconds, dataValue, powerOfTenMultiplier, recordCreate_userName, recordCreate_timeMillis, diff --git a/database/addEnergyData.ts b/database/addEnergyData.ts index a5adc7b..e281481 100644 --- a/database/addEnergyData.ts +++ b/database/addEnergyData.ts @@ -5,6 +5,8 @@ import { getConnectionWhenAvailable } from '../helpers/functions.database.js' import { delay } from '../helpers/functions.utilities.js' import type { EnergyData } from '../types/recordTypes.js' +import { ensureEnergyDataTableExists } from './manageEnergyDataTables.js' + const debug = Debug('emile:database:addEnergyData') export async function addEnergyData( @@ -23,9 +25,13 @@ export async function addEnergyData( try { const rightNowMillis = Date.now() + const tableName = await ensureEnergyDataTableExists( + data.assetId as number + ) + result = emileDB .prepare( - `insert into EnergyData ( + `insert into ${tableName} ( assetId, dataTypeId, fileId, timeSeconds, durationSeconds, dataValue, powerOfTenMultiplier, recordCreate_userName, recordCreate_timeMillis, diff --git a/database/deleteEnergyData.d.ts b/database/deleteEnergyData.d.ts index 93c823c..518a113 100644 --- a/database/deleteEnergyData.d.ts +++ b/database/deleteEnergyData.d.ts @@ -1,2 +1,2 @@ -export declare function deleteEnergyData(dataId: number | string, sessionUser: EmileUser): boolean; +export declare function deleteEnergyData(assetId: number | string, dataId: number | string, sessionUser: EmileUser): Promise; export declare function deleteEnergyDataByFileId(fileId: number | string, sessionUser: EmileUser): Promise; diff --git a/database/deleteEnergyData.js b/database/deleteEnergyData.js index 5eccbf5..009d6f8 100644 --- a/database/deleteEnergyData.js +++ b/database/deleteEnergyData.js @@ -1,9 +1,10 @@ -import sqlite from 'better-sqlite3'; -import { databasePath, getConnectionWhenAvailable } from '../helpers/functions.database.js'; -export function deleteEnergyData(dataId, sessionUser) { - const emileDB = sqlite(databasePath); +import { getConnectionWhenAvailable } from '../helpers/functions.database.js'; +import { ensureEnergyDataTableExists, reloadEnergyDataTableNames } from './manageEnergyDataTables.js'; +export async function deleteEnergyData(assetId, dataId, sessionUser) { + const emileDB = await getConnectionWhenAvailable(); + const tableName = await ensureEnergyDataTableExists(assetId, emileDB); const result = emileDB - .prepare(`update EnergyData + .prepare(`update ${tableName} set recordDelete_userName = ?, recordDelete_timeMillis = ? where recordDelete_timeMillis is null @@ -14,13 +15,18 @@ export function deleteEnergyData(dataId, sessionUser) { } export async function deleteEnergyDataByFileId(fileId, sessionUser) { const emileDB = await getConnectionWhenAvailable(); - const result = emileDB - .prepare(`update EnergyData - set recordDelete_userName = ?, - recordDelete_timeMillis = ? - where recordDelete_timeMillis is null - and fileId = ?`) - .run(sessionUser.userName, Date.now(), fileId); + const tableNames = await reloadEnergyDataTableNames(emileDB); + let count = 0; + for (const tableName of tableNames) { + const result = emileDB + .prepare(`update ${tableName} + set recordDelete_userName = ?, + recordDelete_timeMillis = ? + where recordDelete_timeMillis is null + and fileId = ?`) + .run(sessionUser.userName, Date.now(), fileId); + count += result.changes; + } emileDB.close(); - return result.changes > 0; + return count > 0; } diff --git a/database/deleteEnergyData.ts b/database/deleteEnergyData.ts index 0f6cd53..e791fc1 100644 --- a/database/deleteEnergyData.ts +++ b/database/deleteEnergyData.ts @@ -1,16 +1,22 @@ -import sqlite from 'better-sqlite3' +import { getConnectionWhenAvailable } from '../helpers/functions.database.js' -import { databasePath, getConnectionWhenAvailable } from '../helpers/functions.database.js' +import { + ensureEnergyDataTableExists, + reloadEnergyDataTableNames +} from './manageEnergyDataTables.js' -export function deleteEnergyData( +export async function deleteEnergyData( + assetId: number | string, dataId: number | string, sessionUser: EmileUser -): boolean { - const emileDB = sqlite(databasePath) +): Promise { + const emileDB = await getConnectionWhenAvailable() + + const tableName = await ensureEnergyDataTableExists(assetId, emileDB) const result = emileDB .prepare( - `update EnergyData + `update ${tableName} set recordDelete_userName = ?, recordDelete_timeMillis = ? where recordDelete_timeMillis is null @@ -29,17 +35,25 @@ export async function deleteEnergyDataByFileId( ): Promise { const emileDB = await getConnectionWhenAvailable() - const result = emileDB - .prepare( - `update EnergyData - set recordDelete_userName = ?, - recordDelete_timeMillis = ? - where recordDelete_timeMillis is null - and fileId = ?` - ) - .run(sessionUser.userName, Date.now(), fileId) + const tableNames = await reloadEnergyDataTableNames(emileDB) + + let count = 0 + + for (const tableName of tableNames) { + const result = emileDB + .prepare( + `update ${tableName} + set recordDelete_userName = ?, + recordDelete_timeMillis = ? + where recordDelete_timeMillis is null + and fileId = ?` + ) + .run(sessionUser.userName, Date.now(), fileId) + + count += result.changes + } emileDB.close() - return result.changes > 0 + return count > 0 } diff --git a/database/getEnergyData.js b/database/getEnergyData.js index c7729d3..e8b17e5 100644 --- a/database/getEnergyData.js +++ b/database/getEnergyData.js @@ -2,6 +2,7 @@ import { powerOfTenMultipliers } from '@cityssm/green-button-parser/lookups.js'; import { dateStringToDate } from '@cityssm/utils-datetime'; import sqlite from 'better-sqlite3'; import { databasePath, getConnectionWhenAvailable, getTempTableName } from '../helpers/functions.database.js'; +import { ensureEnergyDataTableExists } from './manageEnergyDataTables.js'; function userFunction_getPowerOfTenMultiplierName(powerOfTenMultiplier) { if (powerOfTenMultiplier === 0) { return ''; @@ -61,7 +62,7 @@ export async function getEnergyData(filters, options) { filters.startDateString !== filters.endDateString; const columnNames = options?.formatForExport ?? false ? `d.dataId, - c.category, a.assetName, + c.category, a.assetName, ts.serviceCategory, userFunction_getPowerOfTenMultiplierName(d.powerOfTenMultiplier) as powerOfTenMultiplierName, tu.unit, @@ -103,9 +104,13 @@ export async function getEnergyData(filters, options) { d.fileId, f.originalFileName, d.timeSeconds, d.durationSeconds, d.endTimeSeconds, d.dataValue, d.powerOfTenMultiplier`; + const emileDB = await getConnectionWhenAvailable(true); + const tableName = (filters.assetId ?? '') === '' + ? 'EnergyData' + : await ensureEnergyDataTableExists(filters.assetId, emileDB); const { sqlParameters, sqlWhereClause } = buildWhereClause(filters); let sql = `select ${columnNames} - from EnergyData d + from ${tableName} d left join Assets a on d.assetId = a.assetId left join AssetCategories c @@ -131,8 +136,9 @@ export async function getEnergyData(filters, options) { sql += " group by d.assetId, d.dataTypeId, substr(datetime(d.timeSeconds, 'unixepoch', 'localtime'), 1, 10), d.powerOfTenMultiplier"; } - const orderBy = ' order by assetId, dataTypeId, timeSeconds'; - const emileDB = await getConnectionWhenAvailable(true); + const orderBy = options?.formatForExport ?? false + ? '' + : ' order by assetId, dataTypeId, timeSeconds'; emileDB.function('userFunction_getPowerOfTenMultiplierName', userFunction_getPowerOfTenMultiplierName); const tempTableName = getTempTableName(); emileDB diff --git a/database/getEnergyData.ts b/database/getEnergyData.ts index 90e2bfe..2f5b062 100644 --- a/database/getEnergyData.ts +++ b/database/getEnergyData.ts @@ -15,6 +15,8 @@ import { } from '../helpers/functions.database.js' import type { EnergyData } from '../types/recordTypes.js' +import { ensureEnergyDataTableExists } from './manageEnergyDataTables.js' + interface GetEnergyDataFilters { assetId?: number | string categoryId?: number | string @@ -124,7 +126,7 @@ export async function getEnergyData( const columnNames = options?.formatForExport ?? false ? `d.dataId, - c.category, a.assetName, + c.category, a.assetName, ts.serviceCategory, userFunction_getPowerOfTenMultiplierName(d.powerOfTenMultiplier) as powerOfTenMultiplierName, tu.unit, @@ -167,10 +169,20 @@ export async function getEnergyData( d.timeSeconds, d.durationSeconds, d.endTimeSeconds, d.dataValue, d.powerOfTenMultiplier` + const emileDB = await getConnectionWhenAvailable(true) + + const tableName = + (filters.assetId ?? '') === '' + ? 'EnergyData' + : await ensureEnergyDataTableExists( + filters.assetId as string | number, + emileDB + ) + const { sqlParameters, sqlWhereClause } = buildWhereClause(filters) let sql = `select ${columnNames} - from EnergyData d + from ${tableName} d left join Assets a on d.assetId = a.assetId left join AssetCategories c @@ -198,9 +210,9 @@ export async function getEnergyData( " group by d.assetId, d.dataTypeId, substr(datetime(d.timeSeconds, 'unixepoch', 'localtime'), 1, 10), d.powerOfTenMultiplier" } - const orderBy = ' order by assetId, dataTypeId, timeSeconds' - - const emileDB = await getConnectionWhenAvailable(true) + const orderBy = options?.formatForExport ?? false + ? '' + : ' order by assetId, dataTypeId, timeSeconds' emileDB.function( 'userFunction_getPowerOfTenMultiplierName', diff --git a/database/initializeDatabase.d.ts b/database/initializeDatabase.d.ts index 871a2de..d42b2a9 100644 --- a/database/initializeDatabase.d.ts +++ b/database/initializeDatabase.d.ts @@ -1 +1,2 @@ -export declare function initializeDatabase(): void; +export declare const recordColumns = " recordCreate_userName varchar(30) not null,\n recordCreate_timeMillis integer not null,\n recordUpdate_userName varchar(30) not null,\n recordUpdate_timeMillis integer not null,\n recordDelete_userName varchar(30),\n recordDelete_timeMillis integer"; +export declare function initializeDatabase(): Promise; diff --git a/database/initializeDatabase.js b/database/initializeDatabase.js index c602966..e43cb74 100644 --- a/database/initializeDatabase.js +++ b/database/initializeDatabase.js @@ -10,6 +10,7 @@ import { addEnergyReadingType } from './addEnergyReadingType.js'; import { addEnergyServiceCategory } from './addEnergyServiceCategory.js'; import { addEnergyUnit } from './addEnergyUnit.js'; import { addUser } from './addUser.js'; +import { refreshEnergyDataTableView } from './manageEnergyDataTables.js'; const debug = Debug('emile:database:initializeDatabase'); const initializeDatabaseUser = { userName: 'system.initialize', @@ -17,7 +18,7 @@ const initializeDatabaseUser = { canUpdate: true, isAdmin: true }; -const recordColumns = ` recordCreate_userName varchar(30) not null, +export const recordColumns = ` recordCreate_userName varchar(30) not null, recordCreate_timeMillis integer not null, recordUpdate_userName varchar(30) not null, recordUpdate_timeMillis integer not null, @@ -217,7 +218,7 @@ function initializeAssetAliasTypes(emileDB) { }, initializeDatabaseUser); } } -export function initializeDatabase() { +export async function initializeDatabase() { const emileDB = sqlite(databasePath); const row = emileDB .prepare(`select name from sqlite_master @@ -299,25 +300,7 @@ export function initializeDatabase() { primary key (groupId, assetId) )`) .run(); - emileDB - .prepare(`create table if not exists EnergyData ( - dataId integer primary key autoincrement, - assetId integer not null references Assets (assetId), - dataTypeId integer not null references EnergyDataTypes (dataTypeId), - fileId integer references EnergyDataFiles (fileId), - timeSeconds integer not null check (timeSeconds > 0), - durationSeconds integer not null check (durationSeconds > 0), - endTimeSeconds integer not null generated always as (timeSeconds + durationSeconds) virtual, - dataValue decimal(10, 2) not null, - powerOfTenMultiplier integer not null default 0, - ${recordColumns} - )`) - .run(); - emileDB - .prepare(`create index if not exists idx_EnergyData on EnergyData - (assetId, dataTypeId, timeSeconds) - where recordDelete_timeMillis is null`) - .run(); + await refreshEnergyDataTableView(emileDB); emileDB .prepare(`create table if not exists Users ( userName varchar(30) primary key, diff --git a/database/initializeDatabase.ts b/database/initializeDatabase.ts index 8f20cfd..098b494 100644 --- a/database/initializeDatabase.ts +++ b/database/initializeDatabase.ts @@ -12,6 +12,7 @@ import { addEnergyReadingType } from './addEnergyReadingType.js' import { addEnergyServiceCategory } from './addEnergyServiceCategory.js' import { addEnergyUnit } from './addEnergyUnit.js' import { addUser } from './addUser.js' +import { refreshEnergyDataTableView } from './manageEnergyDataTables.js' const debug = Debug('emile:database:initializeDatabase') @@ -22,7 +23,7 @@ const initializeDatabaseUser: EmileUser = { isAdmin: true } -const recordColumns = ` recordCreate_userName varchar(30) not null, +export const recordColumns = ` recordCreate_userName varchar(30) not null, recordCreate_timeMillis integer not null, recordUpdate_userName varchar(30) not null, recordUpdate_timeMillis integer not null, @@ -343,7 +344,7 @@ function initializeAssetAliasTypes(emileDB: sqlite.Database): void { } } -export function initializeDatabase(): void { +export async function initializeDatabase(): Promise { const emileDB = sqlite(databasePath) const row = emileDB @@ -480,30 +481,7 @@ export function initializeDatabase(): void { * Energy Data */ - emileDB - .prepare( - `create table if not exists EnergyData ( - dataId integer primary key autoincrement, - assetId integer not null references Assets (assetId), - dataTypeId integer not null references EnergyDataTypes (dataTypeId), - fileId integer references EnergyDataFiles (fileId), - timeSeconds integer not null check (timeSeconds > 0), - durationSeconds integer not null check (durationSeconds > 0), - endTimeSeconds integer not null generated always as (timeSeconds + durationSeconds) virtual, - dataValue decimal(10, 2) not null, - powerOfTenMultiplier integer not null default 0, - ${recordColumns} - )` - ) - .run() - - emileDB - .prepare( - `create index if not exists idx_EnergyData on EnergyData - (assetId, dataTypeId, timeSeconds) - where recordDelete_timeMillis is null` - ) - .run() + await refreshEnergyDataTableView(emileDB) /* * Users diff --git a/database/manageEnergyDataTables.d.ts b/database/manageEnergyDataTables.d.ts new file mode 100644 index 0000000..a9cad99 --- /dev/null +++ b/database/manageEnergyDataTables.d.ts @@ -0,0 +1,4 @@ +import type sqlite from 'better-sqlite3'; +export declare function reloadEnergyDataTableNames(connectedEmileDB?: sqlite.Database): Promise>; +export declare function refreshEnergyDataTableView(connectedEmileDB: sqlite.Database): Promise; +export declare function ensureEnergyDataTableExists(assetId: number | string, connectedEmileDB?: sqlite.Database): Promise; diff --git a/database/manageEnergyDataTables.js b/database/manageEnergyDataTables.js new file mode 100644 index 0000000..90902f9 --- /dev/null +++ b/database/manageEnergyDataTables.js @@ -0,0 +1,107 @@ +import { getConnectionWhenAvailable } from '../helpers/functions.database.js'; +import { recordColumns } from './initializeDatabase.js'; +let energyDataTableNames = new Set(); +function getEnergyDataTableName(assetId) { + return `EnergyData_AssetId_${assetId}`; +} +export async function reloadEnergyDataTableNames(connectedEmileDB) { + const emileDB = connectedEmileDB === undefined + ? await getConnectionWhenAvailable() + : connectedEmileDB; + const result = emileDB + .prepare(`select name from sqlite_master + where type = 'table' + and name like 'EnergyData_AssetId_%'`) + .all(); + if (connectedEmileDB === undefined) { + emileDB.close(); + } + const newEnergyDataTableNames = new Set(); + for (const tableName of result) { + newEnergyDataTableNames.add(tableName.name); + } + energyDataTableNames = newEnergyDataTableNames; + return energyDataTableNames; +} +export async function refreshEnergyDataTableView(connectedEmileDB) { + const emileDB = connectedEmileDB === undefined + ? await getConnectionWhenAvailable() + : connectedEmileDB; + await reloadEnergyDataTableNames(emileDB); + emileDB.prepare('drop view if exists EnergyData').run(); + let createViewSql = ''; + if (energyDataTableNames.size === 0) { + createViewSql = `create view if not exists EnergyData as + select 0 as dataId, + 0 as assetId, + 0 as dataTypeId, + 0 as fileId, + 0 as timeSeconds, + 0 as durationSeconds, + 0 as endTimeSeconds, + 0 as dataValue, + 0 as powerOfTenMultiplier, + 'system.init' as recordCreate_userName, + 0 as recordCreate_timeMillis, + 'system.init' as recordUpdate_userName, + 0 as recordUpdate_timeMillis, + 'system.init' as recordDelete_userName, + 0 as recordDelete_timeMillis + `; + } + else { + const selectStatements = []; + for (const tableName of energyDataTableNames) { + selectStatements.push(`select dataId, assetId, dataTypeId, fileId, + timeSeconds, durationSeconds, endTimeSeconds, + dataValue, powerOfTenMultiplier, + recordCreate_userName, recordCreate_timeMillis, + recordUpdate_userName, recordUpdate_timeMillis, + recordDelete_userName, recordDelete_timeMillis + from ${tableName} + where recordDelete_timeMillis is null`); + } + createViewSql = `create view if not exists EnergyData as + ${selectStatements.join(' union ')}`; + } + emileDB.prepare(createViewSql).run(); + if (connectedEmileDB === undefined) { + emileDB.close(); + } +} +export async function ensureEnergyDataTableExists(assetId, connectedEmileDB) { + if (energyDataTableNames.size === 0) { + await reloadEnergyDataTableNames(connectedEmileDB); + } + const tableName = getEnergyDataTableName(assetId); + if (energyDataTableNames.has(tableName)) { + return tableName; + } + const emileDB = connectedEmileDB === undefined + ? await getConnectionWhenAvailable() + : connectedEmileDB; + emileDB + .prepare(`create table if not exists ${tableName} ( + dataId integer primary key autoincrement, + assetId integer not null references Assets (assetId) check (assetId = ${assetId}), + dataTypeId integer not null references EnergyDataTypes (dataTypeId), + fileId integer references EnergyDataFiles (fileId), + timeSeconds integer not null check (timeSeconds > 0), + durationSeconds integer not null check (durationSeconds > 0), + endTimeSeconds integer not null generated always as (timeSeconds + durationSeconds) virtual, + dataValue decimal(10, 2) not null, + powerOfTenMultiplier integer not null default 0, + ${recordColumns} + )`) + .run(); + emileDB + .prepare(`create index if not exists idx_${tableName} on ${tableName} + (timeSeconds, dataTypeId) + where recordDelete_timeMillis is null`) + .run(); + await refreshEnergyDataTableView(emileDB); + if (connectedEmileDB === undefined) { + emileDB.close(); + } + return tableName; +} diff --git a/database/manageEnergyDataTables.ts b/database/manageEnergyDataTables.ts new file mode 100644 index 0000000..c4fdf70 --- /dev/null +++ b/database/manageEnergyDataTables.ts @@ -0,0 +1,152 @@ +import type sqlite from 'better-sqlite3' + +import { getConnectionWhenAvailable } from '../helpers/functions.database.js' + +import { recordColumns } from './initializeDatabase.js' + +let energyDataTableNames = new Set() + +function getEnergyDataTableName(assetId: number | string): string { + return `EnergyData_AssetId_${assetId}` +} + +export async function reloadEnergyDataTableNames( + connectedEmileDB?: sqlite.Database +): Promise> { + const emileDB = + connectedEmileDB === undefined + ? await getConnectionWhenAvailable() + : connectedEmileDB + + const result = emileDB + .prepare( + `select name from sqlite_master + where type = 'table' + and name like 'EnergyData_AssetId_%'` + ) + .all() as Array<{ name: string }> + + if (connectedEmileDB === undefined) { + emileDB.close() + } + + const newEnergyDataTableNames = new Set() + + for (const tableName of result) { + newEnergyDataTableNames.add(tableName.name) + } + + energyDataTableNames = newEnergyDataTableNames + + return energyDataTableNames +} + +export async function refreshEnergyDataTableView( + connectedEmileDB: sqlite.Database +): Promise { + const emileDB = + connectedEmileDB === undefined + ? await getConnectionWhenAvailable() + : connectedEmileDB + + await reloadEnergyDataTableNames(emileDB) + + emileDB.prepare('drop view if exists EnergyData').run() + + let createViewSql = '' + + if (energyDataTableNames.size === 0) { + createViewSql = `create view if not exists EnergyData as + select 0 as dataId, + 0 as assetId, + 0 as dataTypeId, + 0 as fileId, + 0 as timeSeconds, + 0 as durationSeconds, + 0 as endTimeSeconds, + 0 as dataValue, + 0 as powerOfTenMultiplier, + 'system.init' as recordCreate_userName, + 0 as recordCreate_timeMillis, + 'system.init' as recordUpdate_userName, + 0 as recordUpdate_timeMillis, + 'system.init' as recordDelete_userName, + 0 as recordDelete_timeMillis + ` + } else { + const selectStatements: string[] = [] + + for (const tableName of energyDataTableNames) { + selectStatements.push(`select dataId, assetId, dataTypeId, fileId, + timeSeconds, durationSeconds, endTimeSeconds, + dataValue, powerOfTenMultiplier, + recordCreate_userName, recordCreate_timeMillis, + recordUpdate_userName, recordUpdate_timeMillis, + recordDelete_userName, recordDelete_timeMillis + from ${tableName} + where recordDelete_timeMillis is null`) + } + + createViewSql = `create view if not exists EnergyData as + ${selectStatements.join(' union ')}` + } + + emileDB.prepare(createViewSql).run() + + if (connectedEmileDB === undefined) { + emileDB.close() + } +} + +export async function ensureEnergyDataTableExists( + assetId: number | string, + connectedEmileDB?: sqlite.Database +): Promise { + if (energyDataTableNames.size === 0) { + await reloadEnergyDataTableNames(connectedEmileDB) + } + + const tableName = getEnergyDataTableName(assetId) + + if (energyDataTableNames.has(tableName)) { + return tableName + } + + const emileDB = + connectedEmileDB === undefined + ? await getConnectionWhenAvailable() + : connectedEmileDB + + emileDB + .prepare( + `create table if not exists ${tableName} ( + dataId integer primary key autoincrement, + assetId integer not null references Assets (assetId) check (assetId = ${assetId}), + dataTypeId integer not null references EnergyDataTypes (dataTypeId), + fileId integer references EnergyDataFiles (fileId), + timeSeconds integer not null check (timeSeconds > 0), + durationSeconds integer not null check (durationSeconds > 0), + endTimeSeconds integer not null generated always as (timeSeconds + durationSeconds) virtual, + dataValue decimal(10, 2) not null, + powerOfTenMultiplier integer not null default 0, + ${recordColumns} + )` + ) + .run() + + emileDB + .prepare( + `create index if not exists idx_${tableName} on ${tableName} + (timeSeconds, dataTypeId) + where recordDelete_timeMillis is null` + ) + .run() + + await refreshEnergyDataTableView(emileDB) + + if (connectedEmileDB === undefined) { + emileDB.close() + } + + return tableName +} diff --git a/database/mergeAssets.js b/database/mergeAssets.js index d73d335..3dc6cf9 100644 --- a/database/mergeAssets.js +++ b/database/mergeAssets.js @@ -1,6 +1,7 @@ import { getConnectionWhenAvailable } from '../helpers/functions.database.js'; import { addAsset } from './addAsset.js'; import { deleteAsset } from './deleteAsset.js'; +import { ensureEnergyDataTableExists } from './manageEnergyDataTables.js'; export async function mergeAssets(assetForm, sessionUser) { const emileDB = await getConnectionWhenAvailable(); let latitude; @@ -11,12 +12,13 @@ export async function mergeAssets(assetForm, sessionUser) { latitude = Number.parseFloat(latitudeLongitudeSplit[0]); longitude = Number.parseFloat(latitudeLongitudeSplit[1]); } - const newAssetId = addAsset({ + const newAssetId = await addAsset({ categoryId: Number.parseInt(assetForm.categoryId, 10), assetName: assetForm.assetName, latitude, longitude }, sessionUser, emileDB); + const newAssetTableName = await ensureEnergyDataTableExists(newAssetId); const mergeAssetIds = assetForm.assetIds.split(','); const rightNowMillis = Date.now(); for (const mergeAssetId of mergeAssetIds) { @@ -37,14 +39,32 @@ export async function mergeAssets(assetForm, sessionUser) { and assetId = ? and groupId not in (select groupId from AssetGroupMembers where assetId = ?)`) .run(newAssetId, sessionUser.userName, rightNowMillis, mergeAssetId, newAssetId); + const mergeAssetTableName = await ensureEnergyDataTableExists(mergeAssetId, emileDB); emileDB - .prepare(`update EnergyData - set assetId = ?, - recordUpdate_userName = ?, - recordUpdate_timeMillis = ? - where recordDelete_timeMillis is null - and assetId = ?`) - .run(newAssetId, sessionUser.userName, rightNowMillis, mergeAssetId); + .prepare(`insert into ${newAssetTableName} + (assetId, dataTypeId, fileId, + timeSeconds, durationSeconds, + dataValue, powerOfTenMultiplier, + recordCreate_userName, recordCreate_timeMillis, + recordUpdate_userName, recordUpdate_timeMillis) + + select + ? as assetId, + dataTypeId, fileId, + timeSeconds, durationSeconds, + dataValue, powerOfTenMultiplier, + recordCreate_userName, recordCreate_timeMillis, + ? as recordUpdate_userName, + ? as recordUpdate_timeMillis + from ${mergeAssetTableName} + where recordDelete_timeMillis is null`) + .run(newAssetId, sessionUser.userName, rightNowMillis); + emileDB + .prepare(`update ${mergeAssetTableName} + set recordDelete_userName = ?, + recordDelete_timeMillis = ? + where recordDelete_timeMillis is null`) + .run(sessionUser.userName, rightNowMillis); deleteAsset(mergeAssetId, sessionUser, emileDB); } emileDB.close(); diff --git a/database/mergeAssets.ts b/database/mergeAssets.ts index 98bc067..cf6154a 100644 --- a/database/mergeAssets.ts +++ b/database/mergeAssets.ts @@ -2,6 +2,7 @@ import { getConnectionWhenAvailable } from '../helpers/functions.database.js' import { addAsset } from './addAsset.js' import { deleteAsset } from './deleteAsset.js' +import { ensureEnergyDataTableExists } from './manageEnergyDataTables.js' interface MergeAssetsForm { assetIds: string @@ -28,7 +29,7 @@ export async function mergeAssets( longitude = Number.parseFloat(latitudeLongitudeSplit[1]) } - const newAssetId = addAsset( + const newAssetId = await addAsset( { categoryId: Number.parseInt(assetForm.categoryId, 10), assetName: assetForm.assetName, @@ -39,6 +40,8 @@ export async function mergeAssets( emileDB ) + const newAssetTableName = await ensureEnergyDataTableExists(newAssetId) + const mergeAssetIds = assetForm.assetIds.split(',') const rightNowMillis = Date.now() @@ -75,17 +78,43 @@ export async function mergeAssets( newAssetId ) - // Move over data + const mergeAssetTableName = await ensureEnergyDataTableExists( + mergeAssetId, + emileDB + ) + + // Copy over data emileDB .prepare( - `update EnergyData - set assetId = ?, - recordUpdate_userName = ?, - recordUpdate_timeMillis = ? - where recordDelete_timeMillis is null - and assetId = ?` + `insert into ${newAssetTableName} + (assetId, dataTypeId, fileId, + timeSeconds, durationSeconds, + dataValue, powerOfTenMultiplier, + recordCreate_userName, recordCreate_timeMillis, + recordUpdate_userName, recordUpdate_timeMillis) + + select + ? as assetId, + dataTypeId, fileId, + timeSeconds, durationSeconds, + dataValue, powerOfTenMultiplier, + recordCreate_userName, recordCreate_timeMillis, + ? as recordUpdate_userName, + ? as recordUpdate_timeMillis + from ${mergeAssetTableName} + where recordDelete_timeMillis is null` ) - .run(newAssetId, sessionUser.userName, rightNowMillis, mergeAssetId) + .run(newAssetId, sessionUser.userName, rightNowMillis) + + // Delete old data + emileDB + .prepare( + `update ${mergeAssetTableName} + set recordDelete_userName = ?, + recordDelete_timeMillis = ? + where recordDelete_timeMillis is null` + ) + .run(sessionUser.userName, rightNowMillis) // Delete asset deleteAsset(mergeAssetId, sessionUser, emileDB) diff --git a/handlers/assets-post/doAddAsset.js b/handlers/assets-post/doAddAsset.js index 35f0225..87e0b80 100644 --- a/handlers/assets-post/doAddAsset.js +++ b/handlers/assets-post/doAddAsset.js @@ -1,7 +1,7 @@ import { addAsset } from '../../database/addAsset.js'; import { getAssets } from '../../database/getAssets.js'; export async function handler(request, response) { - const assetId = addAsset(request.body, request.session.user); + const assetId = await addAsset(request.body, request.session.user); const assets = await getAssets({}, { includeEnergyDataStats: true }); response.json({ success: true, diff --git a/handlers/assets-post/doAddAsset.ts b/handlers/assets-post/doAddAsset.ts index ed2419d..199dc38 100644 --- a/handlers/assets-post/doAddAsset.ts +++ b/handlers/assets-post/doAddAsset.ts @@ -4,7 +4,7 @@ import { addAsset } from '../../database/addAsset.js' import { getAssets } from '../../database/getAssets.js' export async function handler(request: Request, response: Response): Promise { - const assetId = addAsset(request.body, request.session.user as EmileUser) + const assetId = await addAsset(request.body, request.session.user as EmileUser) const assets = await getAssets({}, { includeEnergyDataStats: true }) diff --git a/helpers/functions.greenButton.js b/helpers/functions.greenButton.js index 43ed890..df7e609 100644 --- a/helpers/functions.greenButton.js +++ b/helpers/functions.greenButton.js @@ -18,7 +18,7 @@ const greenButtonUser = { canUpdate: true, isAdmin: false }; -function getAssetIdFromIntervalBlock(intervalBlockEntry, connectedEmileDB) { +async function getAssetIdFromIntervalBlock(intervalBlockEntry, connectedEmileDB) { let assetId; let assetAlias = intervalBlockEntry.links.self ?? ''; if (assetAlias === undefined) { @@ -34,7 +34,7 @@ function getAssetIdFromIntervalBlock(intervalBlockEntry, connectedEmileDB) { if (assetCategory === undefined) { throw new Error(`Cannot create asset ${assetAlias} with no asset categories available.`); } - assetId = addAsset({ + assetId = await addAsset({ assetName: assetAlias, categoryId: assetCategory.categoryId }, greenButtonUser, connectedEmileDB); @@ -88,7 +88,7 @@ export async function recordGreenButtonData(greenButtonJson, options) { for (const intervalBlockEntry of intervalBlockEntries) { let assetId = options.assetId; if ((assetId ?? '') === '') { - assetId = getAssetIdFromIntervalBlock(intervalBlockEntry, emileDB); + assetId = await getAssetIdFromIntervalBlock(intervalBlockEntry, emileDB); } const energyDataTypeAndPower = getEnergyDataTypeAndPowerOfTenMultiplier(greenButtonJson, intervalBlockEntry, emileDB); if (energyDataTypeAndPower === undefined || diff --git a/helpers/functions.greenButton.ts b/helpers/functions.greenButton.ts index 066d199..ba697db 100644 --- a/helpers/functions.greenButton.ts +++ b/helpers/functions.greenButton.ts @@ -33,10 +33,10 @@ const greenButtonUser: EmileUser = { isAdmin: false } -function getAssetIdFromIntervalBlock( +async function getAssetIdFromIntervalBlock( intervalBlockEntry: GreenButtonTypes.GreenButtonEntryWithIntervalBlockContent, connectedEmileDB: sqlite.Database -): number { +): Promise { let assetId: number let assetAlias = intervalBlockEntry.links.self ?? '' @@ -70,7 +70,7 @@ function getAssetIdFromIntervalBlock( ) } - assetId = addAsset( + assetId = await addAsset( { assetName: assetAlias, categoryId: assetCategory.categoryId @@ -190,7 +190,7 @@ export async function recordGreenButtonData( let assetId = options.assetId if ((assetId ?? '') === '') { - assetId = getAssetIdFromIntervalBlock(intervalBlockEntry, emileDB) + assetId = await getAssetIdFromIntervalBlock(intervalBlockEntry, emileDB) } /* diff --git a/package-lock.json b/package-lock.json index e1f66e8..0422121 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "@cityssm/express-abuse-points": "^1.1.0", "@cityssm/expressjs-server-js": "^2.3.3", "@cityssm/font-awesome-v5-iconclasses": "^0.1.0", - "@cityssm/green-button-parser": "^0.6.1", + "@cityssm/green-button-parser": "^0.7.0", "@cityssm/green-button-subscriber": "^0.4.0", "@cityssm/is-private-network-address": "^0.2.1", "@cityssm/utils-datetime": "^0.1.1", @@ -49,38 +49,38 @@ "@cityssm/bulma-sticky-table": "^2.1.0", "@cityssm/fa-glow": "^0.1.0", "@cypress/webpack-batteries-included-preprocessor": "^3.0.2", - "@types/activedirectory2": "^1.2.3", + "@types/activedirectory2": "^1.2.4", "@types/better-sqlite3": "^7.6.5", "@types/compression": "^1.7.3", "@types/cookie-parser": "^1.4.4", "@types/csurf": "^1.11.3", - "@types/debug": "^4.1.8", - "@types/ejs": "^3.1.2", - "@types/express": "^4.17.17", - "@types/express-session": "^1.17.7", - "@types/gulp": "^4.0.13", - "@types/gulp-changed": "^0.0.36", - "@types/gulp-minify": "^3.1.2", - "@types/gulp-sass": "^5.0.1", + "@types/debug": "^4.1.9", + "@types/ejs": "^3.1.3", + "@types/express": "^4.17.18", + "@types/express-session": "^1.17.8", + "@types/gulp": "^4.0.14", + "@types/gulp-changed": "^0.0.37", + "@types/gulp-minify": "^3.1.3", + "@types/gulp-sass": "^5.0.2", "@types/http-errors": "^2.0.2", "@types/mocha": "^10.0.1", "@types/multer": "^1.4.7", - "@types/node-windows": "^0.1.3", - "@types/papaparse": "^5.3.8", - "@types/session-file-store": "^1.2.2", + "@types/node-windows": "^0.1.4", + "@types/papaparse": "^5.3.9", + "@types/session-file-store": "^1.2.3", "@types/uuid": "^9.0.4", "bulma": "^0.9.4", "bulma-tooltip": "^3.0.2", "cypress": "^13.2.0", "cypress-axe": "^1.5.0", - "eslint": "^8.49.0", + "eslint": "^8.50.0", "eslint-config-cityssm": "^0.4.0", "gulp": "^4.0.2", "gulp-changed": "^4.0.3", "gulp-minify": "^3.1.0", "gulp-sass": "^5.1.0", "nodemon": "^3.0.1", - "sass": "^1.67.0", + "sass": "^1.68.0", "webpack": "^5.88.2" }, "engines": { @@ -2220,11 +2220,11 @@ } }, "node_modules/@cityssm/green-button-parser": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/@cityssm/green-button-parser/-/green-button-parser-0.6.1.tgz", - "integrity": "sha512-hEvsyR8kFs8ZeKdzLQfWMRCZlQlwTueQak09J6gAkvr9ZinNGsz5cGDkreS+sYuL70D8eriJFzZT9Jb9ZO30ig==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@cityssm/green-button-parser/-/green-button-parser-0.7.0.tgz", + "integrity": "sha512-gvWeGtIwlWCPfdAE+AJhapo5A7QAWdVrjhJNiXSnYA0tu47nnTHaJlEhxMYHDryKFaN6vxV6y4vWW6hvmTfDig==", "dependencies": { - "core-js": "^3.32.0", + "core-js": "^3.32.2", "xml2js": "^0.6.2" }, "engines": { @@ -2244,6 +2244,18 @@ "node": "^14.13.1 || >=16.0.0" } }, + "node_modules/@cityssm/green-button-subscriber/node_modules/@cityssm/green-button-parser": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@cityssm/green-button-parser/-/green-button-parser-0.6.1.tgz", + "integrity": "sha512-hEvsyR8kFs8ZeKdzLQfWMRCZlQlwTueQak09J6gAkvr9ZinNGsz5cGDkreS+sYuL70D8eriJFzZT9Jb9ZO30ig==", + "dependencies": { + "core-js": "^3.32.0", + "xml2js": "^0.6.2" + }, + "engines": { + "node": "^14.13.1 || >=16.0.0" + } + }, "node_modules/@cityssm/is-private-network-address": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/@cityssm/is-private-network-address/-/is-private-network-address-0.2.1.tgz", @@ -2524,9 +2536,9 @@ } }, "node_modules/@eslint/js": { - "version": "8.49.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.49.0.tgz", - "integrity": "sha512-1S8uAY/MTJqVx0SC4epBq+N2yhuwtNwLbJYNZyhL2pO1ZVKn5HFXav5T41Ryzy9K9V7ZId2JB2oy/W4aCd9/2w==", + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.50.0.tgz", + "integrity": "sha512-NCC3zz2+nvYd+Ckfh87rA47zfu2QsQpvc6k1yzTk+b9KzRj0wkGa8LSoGOXN6Zv4lRf/EIoZ80biDh9HOI+RNQ==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -2786,9 +2798,9 @@ } }, "node_modules/@types/activedirectory2": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@types/activedirectory2/-/activedirectory2-1.2.3.tgz", - "integrity": "sha512-yZERTOJFrOAax2HbDyBBhAKyUEa1PC/GXMe9UGBGyeOF0ZRRBKnIMNXVAYfveJMyrhUBhdRoObwe3CBPoekyjQ==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@types/activedirectory2/-/activedirectory2-1.2.4.tgz", + "integrity": "sha512-3LTC0uTbHsmxIFYHcyu1EyM8DRMPbnefBz+py889rAMRtzLciyItaSNOGGosPEco7DKj0zlQEoIlv/M9JXqsWA==", "dev": true, "dependencies": { "@types/ldapjs": "*" @@ -2861,18 +2873,18 @@ } }, "node_modules/@types/debug": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.8.tgz", - "integrity": "sha512-/vPO1EPOs306Cvhwv7KfVfYvOJqA/S/AXjaHQiJboCZzcNDb+TIJFN9/2C9DZ//ijSKWioNyUxD792QmDJ+HKQ==", + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.9.tgz", + "integrity": "sha512-8Hz50m2eoS56ldRlepxSBa6PWEVCtzUo/92HgLc2qTMnotJNIm7xP+UZhyWoYsyOdd5dxZ+NZLb24rsKyFs2ow==", "dev": true, "dependencies": { "@types/ms": "*" } }, "node_modules/@types/ejs": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@types/ejs/-/ejs-3.1.2.tgz", - "integrity": "sha512-ZmiaE3wglXVWBM9fyVC17aGPkLo/UgaOjEiI2FXQfyczrCefORPxIe+2dVmnmk3zkVIbizjrlQzmPGhSYGXG5g==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/ejs/-/ejs-3.1.3.tgz", + "integrity": "sha512-mv5T/JI/bu+pbfz1o+TLl1NF0NIBbjS0Vl6Ppz1YY9DkXfzZT0lelXpfS5i3ZS3U/p90it7uERQpBvLYoK8e4A==", "dev": true }, "node_modules/@types/eslint": { @@ -2908,9 +2920,9 @@ "dev": true }, "node_modules/@types/express": { - "version": "4.17.17", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.17.tgz", - "integrity": "sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q==", + "version": "4.17.18", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.18.tgz", + "integrity": "sha512-Sxv8BSLLgsBYmcnGdGjjEjqET2U+AKAdCRODmMiq02FgjwuV75Ut85DRpvFjyw/Mk0vgUOliGRU0UUmuuZHByQ==", "dev": true, "dependencies": { "@types/body-parser": "*", @@ -2932,9 +2944,9 @@ } }, "node_modules/@types/express-session": { - "version": "1.17.7", - "resolved": "https://registry.npmjs.org/@types/express-session/-/express-session-1.17.7.tgz", - "integrity": "sha512-L25080PBYoRLu472HY/HNCxaXY8AaGgqGC8/p/8+BYMhG0RDOLQ1wpXOpAzr4Gi5TGozTKyJv5BVODM5UNyVMw==", + "version": "1.17.8", + "resolved": "https://registry.npmjs.org/@types/express-session/-/express-session-1.17.8.tgz", + "integrity": "sha512-bFF7/3wOldMn+56XyFRGY9ZzCr3JWhNSP2ajMPgTlbZR6BQOCHdAbNA9W5dMBPgMywpIP4zkmhxP6Opm/NRYMQ==", "dev": true, "dependencies": { "@types/express": "*" @@ -2952,9 +2964,9 @@ } }, "node_modules/@types/gulp": { - "version": "4.0.13", - "resolved": "https://registry.npmjs.org/@types/gulp/-/gulp-4.0.13.tgz", - "integrity": "sha512-Ms20Q2tZ3MpThZGn4Ag6e7ifz/oQJFxsuiopqz5oHmhE6q2ohnELgafi5K/pKX/4ntlpidS61v/TXAguYsVcaA==", + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@types/gulp/-/gulp-4.0.14.tgz", + "integrity": "sha512-zjhBRyGTIPnJ/gByJpjtFyRPKej7zkNFct+QgDhAwpOEoJ2MQjajhU69vrcHIffKcnt3Dik2JHzgMj0C1q9WOg==", "dev": true, "dependencies": { "@types/undertaker": ">=1.2.6", @@ -2963,9 +2975,9 @@ } }, "node_modules/@types/gulp-changed": { - "version": "0.0.36", - "resolved": "https://registry.npmjs.org/@types/gulp-changed/-/gulp-changed-0.0.36.tgz", - "integrity": "sha512-bAXRJVAQRD2LEoLBNUIt3z2SKD9LM4CSv1IcyvjwBIyLpurHQm0zEQKGWVZUxKYqd/Uh30K2SmPnw0x425kjew==", + "version": "0.0.37", + "resolved": "https://registry.npmjs.org/@types/gulp-changed/-/gulp-changed-0.0.37.tgz", + "integrity": "sha512-m/pRr4YgGHwPr/uOozBvtt99+2VdYqsbsFWcWR9SObZkmWYk5AnGjXDk49ooi/4FZE8ZtuH+qYUdP5JqKiTfWg==", "dev": true, "dependencies": { "@types/node": "*", @@ -2973,18 +2985,18 @@ } }, "node_modules/@types/gulp-minify": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@types/gulp-minify/-/gulp-minify-3.1.2.tgz", - "integrity": "sha512-uVfkur2Ml0p/2+YzFZnbqYNmRnP0fomsfbTb8ZKHgAkIclQ3p0V7B58iB7yUoMEj9wxJMZsqQB416a4GmxC3Mg==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/gulp-minify/-/gulp-minify-3.1.3.tgz", + "integrity": "sha512-LDofXULdEf5F6T+SVX2/6aIF1T3N84pu47UBVZSTdTT3sS8j0UKkWykcRlb7AsMQ7YTCVksfzJrs1KBKaX8/ng==", "dev": true, "dependencies": { "@types/node": "*" } }, "node_modules/@types/gulp-sass": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@types/gulp-sass/-/gulp-sass-5.0.1.tgz", - "integrity": "sha512-CRGgIG1U0mqviX8e9J3xkuFv41bvciyE6s4+TiPr49grokV6T/53UZnmOi2LEgbkqx+oZd/d7Ly48k5PIA1mEQ==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/gulp-sass/-/gulp-sass-5.0.2.tgz", + "integrity": "sha512-ns9cehx2D69NZIcomGn91Xxrx9LvCwPjWz1TE/HJ63Q7RKGPgzPOTVZEaiOiHFsL8xMHUpladb+GVKbQQ2+TRw==", "dev": true, "dependencies": { "@types/node": "*", @@ -3073,9 +3085,9 @@ } }, "node_modules/@types/node-windows": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@types/node-windows/-/node-windows-0.1.3.tgz", - "integrity": "sha512-+2NMPagvPWrmUG5sxcABpI1U/JQU1yYLSrG8HkQq4KzrLgJHfdkX0nQv8z0InfvVAMmq4qh/GZya/f6zDK4j6A==", + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@types/node-windows/-/node-windows-0.1.4.tgz", + "integrity": "sha512-yaZS0LdN5vpg2x2Tz254hemRHuVXAZE+6OPS6yFGXxeF1NN1xF8F8+quAoa4BwouBv9o81jOqrabry38K0B+uw==", "dev": true, "dependencies": { "@types/node": "*" @@ -3088,9 +3100,9 @@ "dev": true }, "node_modules/@types/papaparse": { - "version": "5.3.8", - "resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-5.3.8.tgz", - "integrity": "sha512-ArKIEOOWULbhi53wkAiRy1ze4wvrTfhpAj7Yfzva+EkmX2sV8PpFB+xqzJfzXNzK4me95FJH9QZt5NXFVGzOoQ==", + "version": "5.3.9", + "resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-5.3.9.tgz", + "integrity": "sha512-sZcrKD63qA4/6GyBcVvX6AIp0AkpfyYk00CUQHMBvb4+OVXTZWyXUvidUZaai1wyKUVyJoxO7mgREam/pMRrDw==", "dev": true, "dependencies": { "@types/node": "*" @@ -3150,9 +3162,9 @@ } }, "node_modules/@types/session-file-store": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@types/session-file-store/-/session-file-store-1.2.2.tgz", - "integrity": "sha512-l9yZ+PQ8vaXhch03MrV+25BIbhKpeWfZB++3njPIm6lKeDGRS2qF2elLuVa4XrhfJbObqW0puhB3A6FCbkraZg==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@types/session-file-store/-/session-file-store-1.2.3.tgz", + "integrity": "sha512-Oo5DWj5BIUWYvwlLJrgtRuk4Rn5AKtgQk3z9V2JcXgu03/wByS1ODQQpbXuRtlG170iwCGRkV7DELDSu8hVGYA==", "dev": true, "dependencies": { "@types/express": "*", @@ -6824,15 +6836,15 @@ } }, "node_modules/eslint": { - "version": "8.49.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.49.0.tgz", - "integrity": "sha512-jw03ENfm6VJI0jA9U+8H5zfl5b+FvuU3YYvZRdZHOlU2ggJkxrlkJH4HcDrZpj6YwD8kuYqvQM8LyesoazrSOQ==", + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.50.0.tgz", + "integrity": "sha512-FOnOGSuFuFLv/Sa+FDVRZl4GGVAAFFi8LecRsI5a1tMO5HIE8nCm4ivAlzt4dT3ol/PaaGC0rJEEXQmHJBGoOg==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.2", - "@eslint/js": "8.49.0", + "@eslint/js": "8.50.0", "@humanwhocodes/config-array": "^0.11.11", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", @@ -13857,9 +13869,9 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/sass": { - "version": "1.67.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.67.0.tgz", - "integrity": "sha512-SVrO9ZeX/QQyEGtuZYCVxoeAL5vGlYjJ9p4i4HFuekWl8y/LtJ7tJc10Z+ck1c8xOuoBm2MYzcLfTAffD0pl/A==", + "version": "1.68.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.68.0.tgz", + "integrity": "sha512-Lmj9lM/fef0nQswm1J2HJcEsBUba4wgNx2fea6yJHODREoMFnwRpZydBnX/RjyXw2REIwdkbqE4hrTo4qfDBUA==", "dev": true, "dependencies": { "chokidar": ">=3.0.0 <4.0.0", diff --git a/package.json b/package.json index 0cb9525..98d10b5 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "@cityssm/express-abuse-points": "^1.1.0", "@cityssm/expressjs-server-js": "^2.3.3", "@cityssm/font-awesome-v5-iconclasses": "^0.1.0", - "@cityssm/green-button-parser": "^0.6.1", + "@cityssm/green-button-parser": "^0.7.0", "@cityssm/green-button-subscriber": "^0.4.0", "@cityssm/is-private-network-address": "^0.2.1", "@cityssm/utils-datetime": "^0.1.1", @@ -84,38 +84,38 @@ "@cityssm/bulma-sticky-table": "^2.1.0", "@cityssm/fa-glow": "^0.1.0", "@cypress/webpack-batteries-included-preprocessor": "^3.0.2", - "@types/activedirectory2": "^1.2.3", + "@types/activedirectory2": "^1.2.4", "@types/better-sqlite3": "^7.6.5", "@types/compression": "^1.7.3", "@types/cookie-parser": "^1.4.4", "@types/csurf": "^1.11.3", - "@types/debug": "^4.1.8", - "@types/ejs": "^3.1.2", - "@types/express": "^4.17.17", - "@types/express-session": "^1.17.7", - "@types/gulp": "^4.0.13", - "@types/gulp-changed": "^0.0.36", - "@types/gulp-minify": "^3.1.2", - "@types/gulp-sass": "^5.0.1", + "@types/debug": "^4.1.9", + "@types/ejs": "^3.1.3", + "@types/express": "^4.17.18", + "@types/express-session": "^1.17.8", + "@types/gulp": "^4.0.14", + "@types/gulp-changed": "^0.0.37", + "@types/gulp-minify": "^3.1.3", + "@types/gulp-sass": "^5.0.2", "@types/http-errors": "^2.0.2", "@types/mocha": "^10.0.1", "@types/multer": "^1.4.7", - "@types/node-windows": "^0.1.3", - "@types/papaparse": "^5.3.8", - "@types/session-file-store": "^1.2.2", + "@types/node-windows": "^0.1.4", + "@types/papaparse": "^5.3.9", + "@types/session-file-store": "^1.2.3", "@types/uuid": "^9.0.4", "bulma": "^0.9.4", "bulma-tooltip": "^3.0.2", "cypress": "^13.2.0", "cypress-axe": "^1.5.0", - "eslint": "^8.49.0", + "eslint": "^8.50.0", "eslint-config-cityssm": "^0.4.0", "gulp": "^4.0.2", "gulp-changed": "^4.0.3", "gulp-minify": "^3.1.0", "gulp-sass": "^5.1.0", "nodemon": "^3.0.1", - "sass": "^1.67.0", + "sass": "^1.68.0", "webpack": "^5.88.2" } } diff --git a/parsers/sheetParser.js b/parsers/sheetParser.js index 6566891..3f0d55c 100644 --- a/parsers/sheetParser.js +++ b/parsers/sheetParser.js @@ -79,7 +79,7 @@ export class SheetParser extends BaseParser { if (assetCategory === undefined) { throw new Error(`Cannot create asset ${assetAlias} with no asset categories available.`); } - assetId = addAsset({ + assetId = await addAsset({ assetName: assetAlias, categoryId: assetCategory.categoryId }, SheetParser.parserUser, emileDB); diff --git a/parsers/sheetParser.ts b/parsers/sheetParser.ts index cae5a88..1dea517 100644 --- a/parsers/sheetParser.ts +++ b/parsers/sheetParser.ts @@ -148,7 +148,7 @@ export class SheetParser extends BaseParser { ) } - assetId = addAsset( + assetId = await addAsset( { assetName: assetAlias, categoryId: assetCategory.categoryId diff --git a/public-typescript/assets.js b/public-typescript/assets.js index 2ff34bc..d6b7c8c 100644 --- a/public-typescript/assets.js +++ b/public-typescript/assets.js @@ -371,20 +371,22 @@ Object.defineProperty(exports, "__esModule", { value: true }); `; } + const endTimeSecondsDate = new Date(((_e = asset.endTimeSecondsMax) !== null && _e !== void 0 ? _e : 0) * 1000); + const highlightCell = asset.endTimeSecondsMax === null || Date.now() - endTimeSecondsDate.getTime() >= 32 * 86400 * 1000; rowElement.insertAdjacentHTML('beforeend', ` - + ${asset.timeSecondsMin === null ? '(No Data)' - : new Date(((_f = asset.timeSecondsMin) !== null && _f !== void 0 ? _f : 0) * 1000).toLocaleString()} + : new Date(((_g = asset.timeSecondsMin) !== null && _g !== void 0 ? _g : 0) * 1000).toLocaleString()} - + ${asset.endTimeSecondsMax === null ? '' - : new Date(((_g = asset.endTimeSecondsMax) !== null && _g !== void 0 ? _g : 0) * 1000).toLocaleString()} + : endTimeSecondsDate.toLocaleString()} ${((_h = asset.latitude) !== null && _h !== void 0 ? _h : '') === '' || ((_j = asset.longitude) !== null && _j !== void 0 ? _j : '') === '' diff --git a/public-typescript/assets.ts b/public-typescript/assets.ts index 62afa75..c9caf85 100644 --- a/public-typescript/assets.ts +++ b/public-typescript/assets.ts @@ -604,6 +604,9 @@ interface ErrorResponse { ` } + const endTimeSecondsDate = new Date((asset.endTimeSecondsMax ?? 0) * 1000) + const highlightCell = asset.endTimeSecondsMax === null || Date.now() - endTimeSecondsDate.getTime() >= 32 * 86_400 * 1000 + rowElement.insertAdjacentHTML( 'beforeend', ` @@ -620,11 +623,11 @@ interface ErrorResponse { : new Date((asset.timeSecondsMin ?? 0) * 1000).toLocaleString() } - + ${ asset.endTimeSecondsMax === null ? '' - : new Date((asset.endTimeSecondsMax ?? 0) * 1000).toLocaleString() + : endTimeSecondsDate.toLocaleString() } diff --git a/public-typescript/dashboard.js b/public-typescript/dashboard.js index 9406cd0..acd6e1b 100644 --- a/public-typescript/dashboard.js +++ b/public-typescript/dashboard.js @@ -183,7 +183,7 @@ Object.defineProperty(exports, "__esModule", { value: true }); charts[chartKey] = { chart, table, - exportLink: dashboardContainer.querySelector('.is-export-link'), + exportLink: panelElement.querySelector('.is-export-link'), assetId: dataItem.assetId, dataTypeId: dataItem.dataTypeId, timeSecondsMin: dataItem.timeSeconds, diff --git a/public-typescript/dashboard.ts b/public-typescript/dashboard.ts index c6ef866..9e4e3df 100644 --- a/public-typescript/dashboard.ts +++ b/public-typescript/dashboard.ts @@ -290,7 +290,7 @@ declare const cityssm: cityssmGlobal charts[chartKey] = { chart, table, - exportLink: dashboardContainer.querySelector( + exportLink: panelElement.querySelector( '.is-export-link' ) as HTMLAnchorElement, assetId: dataItem.assetId as number, diff --git a/public/javascripts/assets.min.js b/public/javascripts/assets.min.js index baac394..bd4ffab 100644 --- a/public/javascripts/assets.min.js +++ b/public/javascripts/assets.min.js @@ -1 +1 @@ -"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),(()=>{var e;const t=exports.Emile,s=exports.assetAliasTypes,a=document.querySelector("#filter--categoryId"),n=document.querySelector("#filter--assets");function o(e){const s=e.currentTarget.closest("tr"),a=s.dataset.aliasId,n=s.dataset.assetId;bulmaJS.confirm({title:"Delete Asset Alias",message:"Are you sure you want to remove this asset alias?",contextualColorName:"warning",okButton:{text:"Yes, Remove Alias",callbackFunction:function(){cityssm.postJSON(`${t.urlPrefix}/assets/doDeleteAssetAlias`,{aliasId:a,assetId:n},e=>{var t;const s=e;s.success?l(s.assetAliases):bulmaJS.alert({title:"Error Deleting Alias",message:null!==(t=s.errorMessage)&&void 0!==t?t:"Please try again.",contextualColorName:"danger"})})}}})}function l(e){var s,a,n;const l=document.querySelector(".modal #tbody--assetAliases");l.innerHTML="";for(const r of e){const e=document.createElement("tr");e.dataset.aliasId=r.aliasId.toString(),e.dataset.assetId=r.assetId.toString(),e.innerHTML=`\n \n \n ${t.canUpdate?'':""}\n `,e.querySelector('[data-field="aliasType"]').textContent=null!==(s=r.aliasType)&&void 0!==s?s:"",e.querySelector('[data-field="assetAlias"]').textContent=null!==(a=r.assetAlias)&&void 0!==a?a:"",null===(n=e.querySelector(".is-delete-button"))||void 0===n||n.addEventListener("click",o),l.append(e)}}function r(e){e.preventDefault(),cityssm.postJSON(`${t.urlPrefix}/assets/doUpdateAsset`,e.currentTarget,e=>{var s;const a=e;a.success?(t.assets=a.assets,bulmaJS.alert({message:"Asset updated successfully.",contextualColorName:"success"}),m()):bulmaJS.alert({title:"Error Updating Asset",message:null!==(s=a.errorMessage)&&void 0!==s?s:"Please try again.",contextualColorName:"danger"})})}function d(e){e.preventDefault();const s=e.currentTarget;cityssm.postJSON(`${t.urlPrefix}/assets/doAddAssetAlias`,s,e=>{var t;const a=e;a.success?(l(a.assetAliases),s.reset()):bulmaJS.alert({title:"Error Adding Alias",message:null!==(t=a.errorMessage)&&void 0!==t?t:"Please try again.",contextualColorName:"danger"})})}function i(e){let a;function n(s){s.preventDefault(),bulmaJS.confirm({title:"Delete Asset",message:"Are you sure you want to delete this asset?",contextualColorName:"warning",okButton:{text:"Yes, Delete Asset",callbackFunction:function(){cityssm.postJSON(`${t.urlPrefix}/assets/doDeleteAsset`,{assetId:e},e=>{var s;const n=e;n.success?(t.assets=n.assets,m(),a()):bulmaJS.alert({title:"Error Deleting Asset",message:null!==(s=n.errorMessage)&&void 0!==s?s:"",contextualColorName:"danger"})})}}})}cityssm.openHtmlModal("asset-view",{onshow(a){var n;if(function(e,s){cityssm.postJSON(`${t.urlPrefix}/assets/doGetAsset`,{assetId:s},s=>{var a,n,o,r,d,i,c,u,v,g,m,p;const y=s;if(!y.success)return void bulmaJS.alert({title:"Error Loading Asset Details",message:"Please refresh the page and try again.",contextualColorName:"danger"});e.querySelector("#assetView--assetId").value=null!==(n=null===(a=y.asset.assetId)||void 0===a?void 0:a.toString())&&void 0!==n?n:"",e.querySelector('.modal-card-head [data-field="assetName"]').textContent=y.asset.assetName;const f=e.querySelector("#assetView--categoryId");let S=!1;if(t.canUpdate)for(const e of t.assetCategories){const t=document.createElement("option");t.value=null!==(r=null===(o=e.categoryId)||void 0===o?void 0:o.toString())&&void 0!==r?r:"",t.textContent=e.category,e.categoryId===y.asset.categoryId&&(t.selected=!0,S=!0),f.append(t)}if(!S){const e=document.createElement("option");e.value=null!==(i=null===(d=y.asset.categoryId)||void 0===d?void 0:d.toString())&&void 0!==i?i:"",e.textContent=null!==(c=y.asset.category)&&void 0!==c?c:"",e.selected=!0,f.append(e)}e.querySelector("#assetView--assetName").value=y.asset.assetName,e.querySelector("#assetView--latitude").value=null!==(v=null===(u=y.asset.latitude)||void 0===u?void 0:u.toFixed(6))&&void 0!==v?v:"",e.querySelector("#assetView--longitude").value=null!==(m=null===(g=y.asset.longitude)||void 0===g?void 0:g.toFixed(6))&&void 0!==m?m:"",l(null!==(p=y.asset.assetAliases)&&void 0!==p?p:[])})}(a,e),t.canUpdate){a.querySelector("#form--assetView fieldset").disabled=!1,a.querySelector("#assetAliasAdd--assetId").value=e;const t=a.querySelector("#assetAliasAdd--aliasTypeId");for(const e of s){const s=document.createElement("option");s.value=e.aliasTypeId.toString(),s.textContent=e.aliasType,t.append(s)}}else null===(n=a.querySelector("#tbody--assetAliasAdd"))||void 0===n||n.remove()},onshown(e,t){var s,o,l;bulmaJS.toggleHtmlClipped(),bulmaJS.init(e),a=t,null===(s=e.querySelector("#form--assetView"))||void 0===s||s.addEventListener("submit",r),null===(o=e.querySelector(".is-delete-button"))||void 0===o||o.addEventListener("click",n),null===(l=e.querySelector("#form--assetAliasAdd"))||void 0===l||l.addEventListener("submit",d)},onremoved(){bulmaJS.toggleHtmlClipped()}})}function c(e){var t,s;e.preventDefault(),i(null!==(s=null===(t=e.currentTarget.closest("tr"))||void 0===t?void 0:t.dataset.assetId)&&void 0!==s?s:"")}const u=document.querySelector("#button--mergeAssets"),v=document.querySelector("#container--assets");function g(){null!==u&&(u.disabled=v.querySelectorAll("input.selectedAssetId:checked").length<2)}function m(){var e,s,o,l,r,d,i,u,m,p,y,f,S;if(document.querySelector("#count--assets").textContent=t.assets.length.toString(),v.innerHTML="",g(),0===t.assets.length)return void(v.innerHTML='
\n

\n No Assets Found
\n Get started by adding some assets that will be reported on.\n

\n
');const h=n.value.trim().toLowerCase().split(" "),A=document.createElement("table");A.className="table is-fullwidth is-striped has-sticky-header is-fade-hoverable",A.innerHTML=`\n ${t.canUpdate?'Select':""}\n Icon\n Category\n Asset\n Data From\n Data To\n \n \n Map\n \n \n `;e:for(const n of t.assets){if(""!==a.value&&a.value!==(null===(e=n.categoryId)||void 0===e?void 0:e.toString()))continue;const v=`${n.assetName} ${null!==(s=n.category)&&void 0!==s?s:""}`.toLowerCase();for(const e of h)if(!v.includes(e))continue e;const b=document.createElement("tr");b.dataset.assetId=null!==(l=null===(o=n.assetId)||void 0===o?void 0:o.toString())&&void 0!==l?l:"",t.canUpdate&&(b.innerHTML=`\n \n `),b.insertAdjacentHTML("beforeend",`\n \n \n \n \n \n ${null===n.timeSecondsMin?'(No Data)':new Date(1e3*(null!==(d=n.timeSecondsMin)&&void 0!==d?d:0)).toLocaleString()}\n \n \n ${null===n.endTimeSecondsMax?"":new Date(1e3*(null!==(i=n.endTimeSecondsMax)&&void 0!==i?i:0)).toLocaleString()}\n \n \n ${""===(null!==(u=n.latitude)&&void 0!==u?u:"")||""===(null!==(m=n.longitude)&&void 0!==m?m:"")?"":`\n \n ${null!==(p=n.latitude)&&void 0!==p?p:0}, ${null!==(y=n.longitude)&&void 0!==y?y:0}\n `}\n `),b.querySelector('[data-field="category"]').textContent=null!==(f=n.category)&&void 0!==f?f:"";const x=b.querySelector('[data-field="assetName"]');x.textContent=n.assetName,x.addEventListener("click",c),null===(S=b.querySelector("input"))||void 0===S||S.addEventListener("change",g),A.querySelector("tbody").append(b)}0===A.querySelectorAll("tbody tr").length?v.innerHTML='
\n

\n There are no assets that meet your search criteria.
\n Try to be less specific in your search. \n

\n
':v.append(A)}null==u||u.addEventListener("click",()=>{var e,s;const a=v.querySelectorAll("input.selectedAssetId:checked"),n=[];for(const e of a)n.push(e.value);const o=[];for(const a of t.assets)n.includes(null!==(s=null===(e=a.assetId)||void 0===e?void 0:e.toString())&&void 0!==s?s:"")&&o.push(a);let l;function r(e){e.preventDefault(),cityssm.postJSON(`${t.urlPrefix}/assets/doMergeAssets`,e.currentTarget,e=>{const s=e;s.success&&(l(),t.assets=s.assets,m(),i(s.assetId.toString()))})}cityssm.openHtmlModal("asset-merge",{onshow(e){var t,s,a,l,r,d,i,c,u,v,g,m,p;e.querySelector("#assetMerge--assetIds").value=n.join(",");const y=e.querySelector("#assetMerge--categoryId"),f=e.querySelector("#assetMerge--assetName"),S=e.querySelector("#assetMerge--latitudeLongitude");for(const e of o){const n=document.createElement("option");n.textContent=null!==(t=e.category)&&void 0!==t?t:"",n.value=null!==(a=null===(s=e.categoryId)||void 0===s?void 0:s.toString())&&void 0!==a?a:"",y.append(n);const o=document.createElement("option");if(o.textContent=e.assetName,o.value=e.assetName,f.append(o),void 0!==(null!==(l=e.latitude)&&void 0!==l?l:void 0)||void 0!==(null!==(r=e.longitude)&&void 0!==r?r:void 0)){const t=document.createElement("option");t.textContent=`${null!==(i=null===(d=e.latitude)||void 0===d?void 0:d.toString())&&void 0!==i?i:""}, ${null!==(u=null===(c=e.longitude)||void 0===c?void 0:c.toString())&&void 0!==u?u:""}`,t.value=`${null!==(g=null===(v=e.latitude)||void 0===v?void 0:v.toString())&&void 0!==g?g:""}::${null!==(p=null===(m=e.longitude)||void 0===m?void 0:m.toString())&&void 0!==p?p:""}`,S.append(t)}}},onshown(e,t){var s;bulmaJS.toggleHtmlClipped(),l=t,e.querySelector("#assetMerge--categoryId").focus(),null===(s=e.querySelector("form"))||void 0===s||s.addEventListener("submit",r)},onremoved(){bulmaJS.toggleHtmlClipped()}})}),null===(e=document.querySelector("#button--addAsset"))||void 0===e||e.addEventListener("click",()=>{let e;function s(s){s.preventDefault(),cityssm.postJSON(`${t.urlPrefix}/assets/doAddAsset`,s.currentTarget,s=>{const a=s;a.success&&(t.assets=a.assets,m(),e(),i(a.assetId.toString()))})}cityssm.openHtmlModal("asset-add",{onshow(e){var s,a;const n=e.querySelector("#assetAdd--categoryId");for(const e of t.assetCategories){const t=document.createElement("option");t.value=null!==(a=null===(s=e.categoryId)||void 0===s?void 0:s.toString())&&void 0!==a?a:"",t.textContent=e.category,n.append(t)}},onshown(t,a){var n;e=a,bulmaJS.toggleHtmlClipped(),t.querySelector("#assetAdd--categoryId").focus(),null===(n=t.querySelector("form"))||void 0===n||n.addEventListener("submit",s)},onremoved(){bulmaJS.toggleHtmlClipped()}})}),n.addEventListener("keyup",m),a.addEventListener("change",m),m()})(); \ No newline at end of file +"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),(()=>{var e;const t=exports.Emile,s=exports.assetAliasTypes,a=document.querySelector("#filter--categoryId"),n=document.querySelector("#filter--assets");function o(e){const s=e.currentTarget.closest("tr"),a=s.dataset.aliasId,n=s.dataset.assetId;bulmaJS.confirm({title:"Delete Asset Alias",message:"Are you sure you want to remove this asset alias?",contextualColorName:"warning",okButton:{text:"Yes, Remove Alias",callbackFunction:function(){cityssm.postJSON(`${t.urlPrefix}/assets/doDeleteAssetAlias`,{aliasId:a,assetId:n},e=>{var t;const s=e;s.success?l(s.assetAliases):bulmaJS.alert({title:"Error Deleting Alias",message:null!==(t=s.errorMessage)&&void 0!==t?t:"Please try again.",contextualColorName:"danger"})})}}})}function l(e){var s,a,n;const l=document.querySelector(".modal #tbody--assetAliases");l.innerHTML="";for(const r of e){const e=document.createElement("tr");e.dataset.aliasId=r.aliasId.toString(),e.dataset.assetId=r.assetId.toString(),e.innerHTML=`\n \n \n ${t.canUpdate?'':""}\n `,e.querySelector('[data-field="aliasType"]').textContent=null!==(s=r.aliasType)&&void 0!==s?s:"",e.querySelector('[data-field="assetAlias"]').textContent=null!==(a=r.assetAlias)&&void 0!==a?a:"",null===(n=e.querySelector(".is-delete-button"))||void 0===n||n.addEventListener("click",o),l.append(e)}}function r(e){e.preventDefault(),cityssm.postJSON(`${t.urlPrefix}/assets/doUpdateAsset`,e.currentTarget,e=>{var s;const a=e;a.success?(t.assets=a.assets,bulmaJS.alert({message:"Asset updated successfully.",contextualColorName:"success"}),m()):bulmaJS.alert({title:"Error Updating Asset",message:null!==(s=a.errorMessage)&&void 0!==s?s:"Please try again.",contextualColorName:"danger"})})}function d(e){e.preventDefault();const s=e.currentTarget;cityssm.postJSON(`${t.urlPrefix}/assets/doAddAssetAlias`,s,e=>{var t;const a=e;a.success?(l(a.assetAliases),s.reset()):bulmaJS.alert({title:"Error Adding Alias",message:null!==(t=a.errorMessage)&&void 0!==t?t:"Please try again.",contextualColorName:"danger"})})}function i(e){let a;function n(s){s.preventDefault(),bulmaJS.confirm({title:"Delete Asset",message:"Are you sure you want to delete this asset?",contextualColorName:"warning",okButton:{text:"Yes, Delete Asset",callbackFunction:function(){cityssm.postJSON(`${t.urlPrefix}/assets/doDeleteAsset`,{assetId:e},e=>{var s;const n=e;n.success?(t.assets=n.assets,m(),a()):bulmaJS.alert({title:"Error Deleting Asset",message:null!==(s=n.errorMessage)&&void 0!==s?s:"",contextualColorName:"danger"})})}}})}cityssm.openHtmlModal("asset-view",{onshow(a){var n;if(function(e,s){cityssm.postJSON(`${t.urlPrefix}/assets/doGetAsset`,{assetId:s},s=>{var a,n,o,r,d,i,c,u,v,g,m,p;const y=s;if(!y.success)return void bulmaJS.alert({title:"Error Loading Asset Details",message:"Please refresh the page and try again.",contextualColorName:"danger"});e.querySelector("#assetView--assetId").value=null!==(n=null===(a=y.asset.assetId)||void 0===a?void 0:a.toString())&&void 0!==n?n:"",e.querySelector('.modal-card-head [data-field="assetName"]').textContent=y.asset.assetName;const f=e.querySelector("#assetView--categoryId");let S=!1;if(t.canUpdate)for(const e of t.assetCategories){const t=document.createElement("option");t.value=null!==(r=null===(o=e.categoryId)||void 0===o?void 0:o.toString())&&void 0!==r?r:"",t.textContent=e.category,e.categoryId===y.asset.categoryId&&(t.selected=!0,S=!0),f.append(t)}if(!S){const e=document.createElement("option");e.value=null!==(i=null===(d=y.asset.categoryId)||void 0===d?void 0:d.toString())&&void 0!==i?i:"",e.textContent=null!==(c=y.asset.category)&&void 0!==c?c:"",e.selected=!0,f.append(e)}e.querySelector("#assetView--assetName").value=y.asset.assetName,e.querySelector("#assetView--latitude").value=null!==(v=null===(u=y.asset.latitude)||void 0===u?void 0:u.toFixed(6))&&void 0!==v?v:"",e.querySelector("#assetView--longitude").value=null!==(m=null===(g=y.asset.longitude)||void 0===g?void 0:g.toFixed(6))&&void 0!==m?m:"",l(null!==(p=y.asset.assetAliases)&&void 0!==p?p:[])})}(a,e),t.canUpdate){a.querySelector("#form--assetView fieldset").disabled=!1,a.querySelector("#assetAliasAdd--assetId").value=e;const t=a.querySelector("#assetAliasAdd--aliasTypeId");for(const e of s){const s=document.createElement("option");s.value=e.aliasTypeId.toString(),s.textContent=e.aliasType,t.append(s)}}else null===(n=a.querySelector("#tbody--assetAliasAdd"))||void 0===n||n.remove()},onshown(e,t){var s,o,l;bulmaJS.toggleHtmlClipped(),bulmaJS.init(e),a=t,null===(s=e.querySelector("#form--assetView"))||void 0===s||s.addEventListener("submit",r),null===(o=e.querySelector(".is-delete-button"))||void 0===o||o.addEventListener("click",n),null===(l=e.querySelector("#form--assetAliasAdd"))||void 0===l||l.addEventListener("submit",d)},onremoved(){bulmaJS.toggleHtmlClipped()}})}function c(e){var t,s;e.preventDefault(),i(null!==(s=null===(t=e.currentTarget.closest("tr"))||void 0===t?void 0:t.dataset.assetId)&&void 0!==s?s:"")}const u=document.querySelector("#button--mergeAssets"),v=document.querySelector("#container--assets");function g(){null!==u&&(u.disabled=v.querySelectorAll("input.selectedAssetId:checked").length<2)}function m(){var e,s,o,l,r,d,i,u,m,p,y,f,S;if(document.querySelector("#count--assets").textContent=t.assets.length.toString(),v.innerHTML="",g(),0===t.assets.length)return void(v.innerHTML='
\n

\n No Assets Found
\n Get started by adding some assets that will be reported on.\n

\n
');const h=n.value.trim().toLowerCase().split(" "),A=document.createElement("table");A.className="table is-fullwidth is-striped has-sticky-header is-fade-hoverable",A.innerHTML=`\n ${t.canUpdate?'Select':""}\n Icon\n Category\n Asset\n Data From\n Data To\n \n \n Map\n \n \n `;e:for(const n of t.assets){if(""!==a.value&&a.value!==(null===(e=n.categoryId)||void 0===e?void 0:e.toString()))continue;const v=`${n.assetName} ${null!==(s=n.category)&&void 0!==s?s:""}`.toLowerCase();for(const e of h)if(!v.includes(e))continue e;const b=document.createElement("tr");b.dataset.assetId=null!==(l=null===(o=n.assetId)||void 0===o?void 0:o.toString())&&void 0!==l?l:"",t.canUpdate&&(b.innerHTML=`\n \n `);const x=new Date(1e3*(null!==(r=n.endTimeSecondsMax)&&void 0!==r?r:0)),I=null===n.endTimeSecondsMax||Date.now()-x.getTime()>=27648e5;b.insertAdjacentHTML("beforeend",`\n \n \n \n \n \n ${null===n.timeSecondsMin?'(No Data)':new Date(1e3*(null!==(i=n.timeSecondsMin)&&void 0!==i?i:0)).toLocaleString()}\n \n \n ${null===n.endTimeSecondsMax?"":x.toLocaleString()}\n \n \n ${""===(null!==(u=n.latitude)&&void 0!==u?u:"")||""===(null!==(m=n.longitude)&&void 0!==m?m:"")?"":`\n \n ${null!==(p=n.latitude)&&void 0!==p?p:0}, ${null!==(y=n.longitude)&&void 0!==y?y:0}\n `}\n `),b.querySelector('[data-field="category"]').textContent=null!==(f=n.category)&&void 0!==f?f:"";const q=b.querySelector('[data-field="assetName"]');q.textContent=n.assetName,q.addEventListener("click",c),null===(S=b.querySelector("input"))||void 0===S||S.addEventListener("change",g),A.querySelector("tbody").append(b)}0===A.querySelectorAll("tbody tr").length?v.innerHTML='
\n

\n There are no assets that meet your search criteria.
\n Try to be less specific in your search. \n

\n
':v.append(A)}null==u||u.addEventListener("click",()=>{var e,s;const a=v.querySelectorAll("input.selectedAssetId:checked"),n=[];for(const e of a)n.push(e.value);const o=[];for(const a of t.assets)n.includes(null!==(s=null===(e=a.assetId)||void 0===e?void 0:e.toString())&&void 0!==s?s:"")&&o.push(a);let l;function r(e){e.preventDefault(),cityssm.postJSON(`${t.urlPrefix}/assets/doMergeAssets`,e.currentTarget,e=>{const s=e;s.success&&(l(),t.assets=s.assets,m(),i(s.assetId.toString()))})}cityssm.openHtmlModal("asset-merge",{onshow(e){var t,s,a,l,r,d,i,c,u,v,g,m,p;e.querySelector("#assetMerge--assetIds").value=n.join(",");const y=e.querySelector("#assetMerge--categoryId"),f=e.querySelector("#assetMerge--assetName"),S=e.querySelector("#assetMerge--latitudeLongitude");for(const e of o){const n=document.createElement("option");n.textContent=null!==(t=e.category)&&void 0!==t?t:"",n.value=null!==(a=null===(s=e.categoryId)||void 0===s?void 0:s.toString())&&void 0!==a?a:"",y.append(n);const o=document.createElement("option");if(o.textContent=e.assetName,o.value=e.assetName,f.append(o),void 0!==(null!==(l=e.latitude)&&void 0!==l?l:void 0)||void 0!==(null!==(r=e.longitude)&&void 0!==r?r:void 0)){const t=document.createElement("option");t.textContent=`${null!==(i=null===(d=e.latitude)||void 0===d?void 0:d.toString())&&void 0!==i?i:""}, ${null!==(u=null===(c=e.longitude)||void 0===c?void 0:c.toString())&&void 0!==u?u:""}`,t.value=`${null!==(g=null===(v=e.latitude)||void 0===v?void 0:v.toString())&&void 0!==g?g:""}::${null!==(p=null===(m=e.longitude)||void 0===m?void 0:m.toString())&&void 0!==p?p:""}`,S.append(t)}}},onshown(e,t){var s;bulmaJS.toggleHtmlClipped(),l=t,e.querySelector("#assetMerge--categoryId").focus(),null===(s=e.querySelector("form"))||void 0===s||s.addEventListener("submit",r)},onremoved(){bulmaJS.toggleHtmlClipped()}})}),null===(e=document.querySelector("#button--addAsset"))||void 0===e||e.addEventListener("click",()=>{let e;function s(s){s.preventDefault(),cityssm.postJSON(`${t.urlPrefix}/assets/doAddAsset`,s.currentTarget,s=>{const a=s;a.success&&(t.assets=a.assets,m(),e(),i(a.assetId.toString()))})}cityssm.openHtmlModal("asset-add",{onshow(e){var s,a;const n=e.querySelector("#assetAdd--categoryId");for(const e of t.assetCategories){const t=document.createElement("option");t.value=null!==(a=null===(s=e.categoryId)||void 0===s?void 0:s.toString())&&void 0!==a?a:"",t.textContent=e.category,n.append(t)}},onshown(t,a){var n;e=a,bulmaJS.toggleHtmlClipped(),t.querySelector("#assetAdd--categoryId").focus(),null===(n=t.querySelector("form"))||void 0===n||n.addEventListener("submit",s)},onremoved(){bulmaJS.toggleHtmlClipped()}})}),n.addEventListener("keyup",m),a.addEventListener("change",m),m()})(); \ No newline at end of file diff --git a/public/javascripts/dashboard.min.js b/public/javascripts/dashboard.min.js index 4adf6d0..737d318 100644 --- a/public/javascripts/dashboard.min.js +++ b/public/javascripts/dashboard.min.js @@ -1 +1 @@ -"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),(()=>{const e=exports.Emile,t=document.querySelector("#container--dashboard"),a=document.querySelector("#form--dashboard"),n="dashboard--startDateString",s=a.querySelector(`#${n}`),i=sessionStorage.getItem(n);null!==i&&(s.value=i);const l="dashboard--endDateString",d=a.querySelector(`#${l}`),r=sessionStorage.getItem(l);null!==r&&(d.value=r);const o=a.querySelector('button[type="submit"]');let c={};function u(e,t){return`${e}_${t}`}function h(e){return new Date(1e3*e).toLocaleString()}function v(e,t,a){e.data.labels.push(t);for(const t of e.data.datasets)t.data.push(a)}function m(e,t){var a,n,s,i,l;const d=document.createElement("tr");d.innerHTML=`${h(t.timeSeconds)}\n ${function(e){let t=e,a="s";return t>=60?(a="min",(t/=60)>=60?(a="hr",(t/=60)>=24&&(t/=24,a="days"),`${t} ${a}`):`${t} ${a}`):`${t} ${a}`}(t.durationSeconds)}\n ${null!==(a=t.readingType)&&void 0!==a?a:""}\n ${null!==(n=t.commodity)&&void 0!==n?n:""}\n ${t.dataValue}\n ${null!==(s=t.powerOfTenMultiplierName)&&void 0!==s?s:""}\n ${null!==(i=t.unit)&&void 0!==i?i:""}`,null===(l=e.querySelector("tbody"))||void 0===l||l.append(d)}function p(){s.value>d.value?t.innerHTML='
\n

The start date must be less than or equal to the end date.

\n
':(o.disabled=!0,t.innerHTML='

\n
\n Loading data...\n

',sessionStorage.setItem(n,s.value),sessionStorage.setItem(l,d.value),cityssm.postJSON(`${e.urlPrefix}/dashboard/doGetEnergyData`,a,a=>{var n,s,i,l,d,r;const o=a;t.innerHTML="",c={},0===o.energyData.length&&(t.innerHTML='
\n

There is no energy data that meets your search criteria.

\n
');for(const e of o.energyData){const a=u(e.assetId,e.dataTypeId),o=e.dataValue*Math.pow(10,e.powerOfTenMultiplier)/Math.pow(10,null!==(n=e.preferredPowerOfTenMultiplier)&&void 0!==n?n:0),p=`${null!==(s=e.preferredPowerOfTenMultiplierName)&&void 0!==s?s:""} ${null!==(i=e.unit)&&void 0!==i?i:""}`;if(void 0===c[a]){const n=document.createElement("div");n.className="panel",n.innerHTML=`
\n
\n
\n
\n \n
\n
\n

\n \n
\n
\n
\n
\n
\n \n
\n
\n \n
\n
\n
\n
\n
\n \n
\n \n
`,n.querySelector('[data-field="assetName"]').textContent=null!==(d=e.assetName)&&void 0!==d?d:"",n.querySelector('[data-field="serviceCategory"]').textContent=null!==(r=e.serviceCategory)&&void 0!==r?r:"",t.append(n);const s=new Chart(n.querySelector("canvas"),{type:"bar",data:{labels:[h(e.timeSeconds)],datasets:[{label:p,data:[o]}]}}),i=n.querySelector("table");c[a]={chart:s,table:i,exportLink:t.querySelector(".is-export-link"),assetId:e.assetId,dataTypeId:e.dataTypeId,timeSecondsMin:e.timeSeconds,timeSecondsMax:e.timeSeconds},m(i,e)}else v(c[a].chart,h(e.timeSeconds),o),m(c[a].table,e),c[a].timeSecondsMin=Math.min(c[a].timeSecondsMin,e.timeSeconds),c[a].timeSecondsMax=Math.max(c[a].timeSecondsMax,e.timeSeconds)}for(const t of Object.values(c))t.chart.update(),t.exportLink.href=`${e.urlPrefix}/reports/energyData-formatted-filtered/?assetId=${t.assetId}&dataTypeId=${t.dataTypeId}&timeSecondsMin=${t.timeSecondsMin}&timeSecondsMax=${t.timeSecondsMax}`;bulmaJS.init(t)}))}function b(){o.disabled=!1}e.initializeAssetSelector({assetSelectorElement:a.querySelector("#dashboard--assetSelector"),callbackFunction:p}),s.addEventListener("change",b),d.addEventListener("change",b),a.addEventListener("submit",e=>{e.preventDefault(),p()}),p()})(); \ No newline at end of file +"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),(()=>{const e=exports.Emile,t=document.querySelector("#container--dashboard"),a=document.querySelector("#form--dashboard"),n="dashboard--startDateString",s=a.querySelector(`#${n}`),i=sessionStorage.getItem(n);null!==i&&(s.value=i);const l="dashboard--endDateString",d=a.querySelector(`#${l}`),r=sessionStorage.getItem(l);null!==r&&(d.value=r);const o=a.querySelector('button[type="submit"]');let c={};function u(e,t){return`${e}_${t}`}function h(e){return new Date(1e3*e).toLocaleString()}function v(e,t,a){e.data.labels.push(t);for(const t of e.data.datasets)t.data.push(a)}function m(e,t){var a,n,s,i,l;const d=document.createElement("tr");d.innerHTML=`${h(t.timeSeconds)}\n ${function(e){let t=e,a="s";return t>=60?(a="min",(t/=60)>=60?(a="hr",(t/=60)>=24&&(t/=24,a="days"),`${t} ${a}`):`${t} ${a}`):`${t} ${a}`}(t.durationSeconds)}\n ${null!==(a=t.readingType)&&void 0!==a?a:""}\n ${null!==(n=t.commodity)&&void 0!==n?n:""}\n ${t.dataValue}\n ${null!==(s=t.powerOfTenMultiplierName)&&void 0!==s?s:""}\n ${null!==(i=t.unit)&&void 0!==i?i:""}`,null===(l=e.querySelector("tbody"))||void 0===l||l.append(d)}function p(){s.value>d.value?t.innerHTML='
\n

The start date must be less than or equal to the end date.

\n
':(o.disabled=!0,t.innerHTML='

\n
\n Loading data...\n

',sessionStorage.setItem(n,s.value),sessionStorage.setItem(l,d.value),cityssm.postJSON(`${e.urlPrefix}/dashboard/doGetEnergyData`,a,a=>{var n,s,i,l,d,r;const o=a;t.innerHTML="",c={},0===o.energyData.length&&(t.innerHTML='
\n

There is no energy data that meets your search criteria.

\n
');for(const e of o.energyData){const a=u(e.assetId,e.dataTypeId),o=e.dataValue*Math.pow(10,e.powerOfTenMultiplier)/Math.pow(10,null!==(n=e.preferredPowerOfTenMultiplier)&&void 0!==n?n:0),p=`${null!==(s=e.preferredPowerOfTenMultiplierName)&&void 0!==s?s:""} ${null!==(i=e.unit)&&void 0!==i?i:""}`;if(void 0===c[a]){const n=document.createElement("div");n.className="panel",n.innerHTML=`
\n
\n
\n
\n \n
\n
\n

\n \n
\n
\n
\n
\n
\n \n
\n
\n \n
\n
\n
\n
\n
\n \n
\n \n
`,n.querySelector('[data-field="assetName"]').textContent=null!==(d=e.assetName)&&void 0!==d?d:"",n.querySelector('[data-field="serviceCategory"]').textContent=null!==(r=e.serviceCategory)&&void 0!==r?r:"",t.append(n);const s=new Chart(n.querySelector("canvas"),{type:"bar",data:{labels:[h(e.timeSeconds)],datasets:[{label:p,data:[o]}]}}),i=n.querySelector("table");c[a]={chart:s,table:i,exportLink:n.querySelector(".is-export-link"),assetId:e.assetId,dataTypeId:e.dataTypeId,timeSecondsMin:e.timeSeconds,timeSecondsMax:e.timeSeconds},m(i,e)}else v(c[a].chart,h(e.timeSeconds),o),m(c[a].table,e),c[a].timeSecondsMin=Math.min(c[a].timeSecondsMin,e.timeSeconds),c[a].timeSecondsMax=Math.max(c[a].timeSecondsMax,e.timeSeconds)}for(const t of Object.values(c))t.chart.update(),t.exportLink.href=`${e.urlPrefix}/reports/energyData-formatted-filtered/?assetId=${t.assetId}&dataTypeId=${t.dataTypeId}&timeSecondsMin=${t.timeSecondsMin}&timeSecondsMax=${t.timeSecondsMax}`;bulmaJS.init(t)}))}function b(){o.disabled=!1}e.initializeAssetSelector({assetSelectorElement:a.querySelector("#dashboard--assetSelector"),callbackFunction:p}),s.addEventListener("change",b),d.addEventListener("change",b),a.addEventListener("submit",e=>{e.preventDefault(),p()}),p()})(); \ No newline at end of file diff --git a/routes/admin.ts b/routes/admin.ts index e654063..dc1c41a 100644 --- a/routes/admin.ts +++ b/routes/admin.ts @@ -41,7 +41,7 @@ router.post('/doDeleteUser', handler_doDeleteUser) router.get('/database', handler_database as RequestHandler) router.post('/doBackupDatabase', handler_doBackupDatabase as RequestHandler) -router.post('/doCleanupDatabase', handler_doCleanupDatabase) +router.post('/doCleanupDatabase', handler_doCleanupDatabase as RequestHandler) router.post('/doDeleteBackupFile', handler_doDeleteBackupFile as RequestHandler) diff --git a/routes/assets.ts b/routes/assets.ts index 6c5b965..acbed45 100644 --- a/routes/assets.ts +++ b/routes/assets.ts @@ -1,4 +1,4 @@ -import { Router } from 'express' +import { type RequestHandler, Router } from 'express' import handler_assets from '../handlers/assets-get/assets.js' import handler_doAddAsset from '../handlers/assets-post/doAddAsset.js' @@ -18,19 +18,35 @@ import { updatePostHandler } from '../handlers/permissions.js' export const router = Router() -router.get('/', handler_assets) +router.get('/', handler_assets as RequestHandler) // Assets -router.post('/doAddAsset', updatePostHandler, handler_doAddAsset) +router.post( + '/doAddAsset', + updatePostHandler, + handler_doAddAsset as RequestHandler +) router.post('/doGetAsset', handler_doGetAsset) -router.post('/doUpdateAsset', updatePostHandler, handler_doUpdateAsset) +router.post( + '/doUpdateAsset', + updatePostHandler, + handler_doUpdateAsset as RequestHandler +) -router.post('/doMergeAssets', updatePostHandler, handler_doMergeAssets) +router.post( + '/doMergeAssets', + updatePostHandler, + handler_doMergeAssets as RequestHandler +) -router.post('/doDeleteAsset', updatePostHandler, handler_doDeleteAsset) +router.post( + '/doDeleteAsset', + updatePostHandler, + handler_doDeleteAsset as RequestHandler +) // Asset Aliases @@ -46,7 +62,7 @@ router.post( router.post('/doAddAssetGroup', updatePostHandler, handler_doAddAssetGroup) -router.post('/doGetAssetGroup', handler_doGetAssetGroup) +router.post('/doGetAssetGroup', handler_doGetAssetGroup as RequestHandler) router.post( '/doUpdateAssetGroup', @@ -57,13 +73,13 @@ router.post( router.post( '/doAddAssetGroupMember', updatePostHandler, - handler_doAddAssetGroupMember + handler_doAddAssetGroupMember as RequestHandler ) router.post( '/doDeleteAssetGroupMember', updatePostHandler, - handler_doDeleteAssetGroupMember + handler_doDeleteAssetGroupMember as RequestHandler ) router.post( diff --git a/routes/dashboard.ts b/routes/dashboard.ts index b2c9480..ff8bba6 100644 --- a/routes/dashboard.ts +++ b/routes/dashboard.ts @@ -1,12 +1,12 @@ -import { Router } from 'express' +import { type RequestHandler, Router } from 'express' import handler_dashboard from '../handlers/dashboard-get/dashboard.js' import handler_doGetEnergyData from '../handlers/dashboard-post/doGetEnergyData.js' export const router = Router() -router.get('/', handler_dashboard) +router.get('/', handler_dashboard as RequestHandler) -router.post('/doGetEnergyData', handler_doGetEnergyData) +router.post('/doGetEnergyData', handler_doGetEnergyData as RequestHandler) export default router diff --git a/routes/data.ts b/routes/data.ts index 3428cc9..0435a8b 100644 --- a/routes/data.ts +++ b/routes/data.ts @@ -1,4 +1,4 @@ -import { Router } from 'express' +import { type RequestHandler, Router } from 'express' import handler_data from '../handlers/data-get/data.js' import handler_doDeletePendingEnergyDataFile from '../handlers/data-post/doDeletePendingEnergyDataFile.js' @@ -11,42 +11,39 @@ import { handlers as handlers_doUploadDataFiles } from '../handlers/data-post/do export const router = Router() -router.get('/', handler_data) +router.get('/', handler_data as RequestHandler) router.post( '/doUploadDataFiles', handlers_doUploadDataFiles.uploadHander.array('file'), - handlers_doUploadDataFiles.successHandler + handlers_doUploadDataFiles.successHandler as RequestHandler ) router.post( '/doUpdatePendingEnergyDataFile', - handler_doUpdatePendingEnergyDataFile + handler_doUpdatePendingEnergyDataFile as RequestHandler ) -router.post( - '/doGetPendingFiles', - handler_doGetPendingFiles -) +router.post('/doGetPendingFiles', handler_doGetPendingFiles as RequestHandler) router.post( '/doProcessPendingEnergyDataFile', - handler_doProcessPendingEnergyDataFile + handler_doProcessPendingEnergyDataFile as RequestHandler ) router.post( '/doDeletePendingEnergyDataFile', - handler_doDeletePendingEnergyDataFile + handler_doDeletePendingEnergyDataFile as RequestHandler ) router.post( '/doReprocessProcessedEnergyDataFile', - handler_doReprocessProcessedEnergyDataFile + handler_doReprocessProcessedEnergyDataFile as RequestHandler ) router.post( '/doDeleteProcessedEnergyDataFile', - handler_doDeleteProcessedEnergyDataFile + handler_doDeleteProcessedEnergyDataFile as RequestHandler ) export default router diff --git a/routes/reports.ts b/routes/reports.ts index 971faff..c6b6425 100644 --- a/routes/reports.ts +++ b/routes/reports.ts @@ -1,4 +1,10 @@ -import { type NextFunction, type Request, type Response, Router } from 'express' +import { + type NextFunction, + type Request, + type Response, + Router, + type RequestHandler +} from 'express' import { isValidUserReportKey } from '../database/isValidUserReportKey.js' import handler_reportName from '../handlers/reports-get/reportName.js' @@ -44,8 +50,12 @@ function sessionOrReportKeyHandler( ) } -router.get('/', sessionHandler, handler_reports) +router.get('/', sessionHandler, handler_reports as RequestHandler) -router.all('/:reportName', sessionOrReportKeyHandler, handler_reportName) +router.all( + '/:reportName', + sessionOrReportKeyHandler, + handler_reportName as RequestHandler +) export default router diff --git a/tasks/greenButtonCMDProcessor.js b/tasks/greenButtonCMDProcessor.js index 9276c7c..ca1d01b 100644 --- a/tasks/greenButtonCMDProcessor.js +++ b/tasks/greenButtonCMDProcessor.js @@ -35,6 +35,9 @@ async function processGreenButtonSubscriptions() { break; } debug(`Loading authorizations for subscription: ${subscriptionKey} ...`); + if (updatedMins[subscriptionKey] === undefined) { + updatedMins[subscriptionKey] = {}; + } const greenButtonSubscriber = new GreenButtonSubscriber(greenButtonSubscription.configuration); const authorizations = await greenButtonSubscriber.getAuthorizations(); if (authorizations === undefined) { @@ -56,7 +59,7 @@ async function processGreenButtonSubscriptions() { debug(`Skipping authorization id: ${subscriptionKey}, ${authorizationId}`); continue; } - const updatedMinMillis = updatedMins[authorizationId]; + const updatedMinMillis = updatedMins[subscriptionKey][authorizationId]; let updatedMin; if ((updatedMinMillis ?? undefined) === undefined) { updatedMin = new Date(); @@ -78,7 +81,8 @@ async function processGreenButtonSubscriptions() { } try { await recordGreenButtonData(usageData, {}); - updatedMins[authorizationId] = usageData.updatedDate?.getTime() ?? 0; + updatedMins[subscriptionKey][authorizationId] = + usageData.updatedDate?.getTime() ?? 0; saveCache(); } catch (error) { diff --git a/tasks/greenButtonCMDProcessor.ts b/tasks/greenButtonCMDProcessor.ts index 2be9096..f183aa1 100644 --- a/tasks/greenButtonCMDProcessor.ts +++ b/tasks/greenButtonCMDProcessor.ts @@ -3,6 +3,7 @@ import fs from 'node:fs' import { helpers as greenButtonHelpers } from '@cityssm/green-button-parser' +import type { GreenButtonJson } from '@cityssm/green-button-parser/types/entryTypes.js' import { GreenButtonSubscriber } from '@cityssm/green-button-subscriber' import Debug from 'debug' import exitHook from 'exit-hook' @@ -21,7 +22,7 @@ const pollingIntervalMillis = 86_400 * 1000 + 60_000 const updatedMinsCacheFile = 'data/caches/greenButtonCMDProcessor.json' -let updatedMins: Record = {} +let updatedMins: Record> = {} try { updatedMins = JSON.parse( @@ -62,6 +63,10 @@ async function processGreenButtonSubscriptions(): Promise { debug(`Loading authorizations for subscription: ${subscriptionKey} ...`) + if (updatedMins[subscriptionKey] === undefined) { + updatedMins[subscriptionKey] = {} + } + const greenButtonSubscriber = new GreenButtonSubscriber( greenButtonSubscription.configuration ) @@ -74,7 +79,7 @@ async function processGreenButtonSubscriptions(): Promise { } const entries = greenButtonHelpers.getEntriesByContentType( - authorizations, + authorizations as GreenButtonJson, 'Authorization' ) @@ -103,7 +108,7 @@ async function processGreenButtonSubscriptions(): Promise { continue } - const updatedMinMillis = updatedMins[authorizationId] + const updatedMinMillis = updatedMins[subscriptionKey][authorizationId] let updatedMin: Date if ((updatedMinMillis ?? undefined) === undefined) { @@ -136,8 +141,9 @@ async function processGreenButtonSubscriptions(): Promise { } try { - await recordGreenButtonData(usageData, {}) - updatedMins[authorizationId] = usageData.updatedDate?.getTime() ?? 0 + await recordGreenButtonData(usageData as GreenButtonJson, {}) + updatedMins[subscriptionKey][authorizationId] = + usageData.updatedDate?.getTime() ?? 0 saveCache() } catch (error) { debug(`Error recording data: ${subscriptionKey}, ${authorizationId}`) diff --git a/temp/updateAssetNames.js b/temp/updateAssetNames.js index ecf8e8f..632bdc8 100644 --- a/temp/updateAssetNames.js +++ b/temp/updateAssetNames.js @@ -1,8 +1,5 @@ import sqlite from 'better-sqlite3'; import XLSX from 'xlsx'; -import { addAsset } from '../database/addAsset.js'; -import { addAssetAlias } from '../database/addAssetAlias.js'; -import { getAssetByAssetAlias } from '../database/getAsset.js'; import { getAssetAliasTypeByAliasTypeKey } from '../database/getAssetAliasType.js'; import { getAssetCategories } from '../database/getAssetCategories.js'; import { databasePath } from '../helpers/functions.database.js'; @@ -12,7 +9,7 @@ const updateUser = { canUpdate: true, isAdmin: false }; -function updateSsmPucAssetNames() { +async function updateSsmPucAssetNames() { const workbook = XLSX.readFile('./temp/assetNames.xlsx', {}); const worksheet = workbook.Sheets[workbook.SheetNames[0]]; const assetRows = XLSX.utils.sheet_to_json(worksheet, { @@ -27,10 +24,10 @@ function updateSsmPucAssetNames() { const assetCategory = assetCategories.find((possibleCategory) => { return possibleCategory.category === assetRow.category.trim(); }); - let hasElectricityAccountNumber = false; - if (assetRow.accountNumberElectricity !== undefined && - assetRow.accountNumberElectricity !== '') { - hasElectricityAccountNumber = true; + let hasUtilityApiAuthorizationNumber = false; + if (assetRow.utilityApiAuthorizationNumber !== undefined && + assetRow.utilityApiAuthorizationNumber !== '') { + hasUtilityApiAuthorizationNumber = true; emileDB .prepare(`update Assets set categoryId = ?, @@ -38,32 +35,10 @@ function updateSsmPucAssetNames() { recordUpdate_userName = ?, recordUpdate_timeMillis = ? where recordDelete_timeMillis is null - and assetName = ?`) - .run(assetCategory?.categoryId, assetRow.assetName.trim(), updateUser.userName, Date.now(), assetRow.accountNumberElectricity.toString()); - } - if (assetRow.accountNumberGas !== undefined && - assetRow.accountNumberGas !== '') { - let assetId; - const gasAccountNumber = assetRow.accountNumberGas.replace("'", ''); - if (hasElectricityAccountNumber) { - const asset = getAssetByAssetAlias(assetRow.accountNumberElectricity?.toString() ?? '', electricityAccountNumberAlias?.aliasTypeId); - if (asset !== undefined) { - assetId = asset.assetId ?? 0; - } - } - if (assetId === undefined) { - assetId = addAsset({ - assetName: assetRow.assetName, - categoryId: assetCategory?.categoryId - }, updateUser, emileDB); - } - addAssetAlias({ - aliasTypeId: gasAccountNumberAlias?.aliasTypeId, - assetId: assetId ?? 0, - assetAlias: gasAccountNumber - }, updateUser, emileDB); + and assetName like 'https://utilityapi.com/DataCustodian/espi/1_1/resource/Subscription/${assetRow.utilityApiAuthorizationNumber}/%'`) + .run(assetCategory?.categoryId, assetRow.assetName.trim(), updateUser.userName, Date.now()); } } emileDB.close(); } -updateSsmPucAssetNames(); +await updateSsmPucAssetNames(); diff --git a/temp/updateAssetNames.ts b/temp/updateAssetNames.ts index 9dbe9bc..64cf916 100644 --- a/temp/updateAssetNames.ts +++ b/temp/updateAssetNames.ts @@ -13,6 +13,7 @@ interface AssetRow { assetName: string address: string accountNumberElectricity?: number | '' + utilityApiAuthorizationNumber?: number | '' accountNumberGas?: string services: string } @@ -24,7 +25,7 @@ const updateUser = { isAdmin: false } -function updateSsmPucAssetNames(): void { +async function updateSsmPucAssetNames(): Promise { const workbook = XLSX.readFile('./temp/assetNames.xlsx', {}) const worksheet = workbook.Sheets[workbook.SheetNames[0]] @@ -51,13 +52,13 @@ function updateSsmPucAssetNames(): void { return possibleCategory.category === assetRow.category.trim() }) - let hasElectricityAccountNumber = false + let hasUtilityApiAuthorizationNumber = false if ( - assetRow.accountNumberElectricity !== undefined && - assetRow.accountNumberElectricity !== '' + assetRow.utilityApiAuthorizationNumber !== undefined && + assetRow.utilityApiAuthorizationNumber !== '' ) { - hasElectricityAccountNumber = true + hasUtilityApiAuthorizationNumber = true emileDB .prepare( @@ -67,17 +68,17 @@ function updateSsmPucAssetNames(): void { recordUpdate_userName = ?, recordUpdate_timeMillis = ? where recordDelete_timeMillis is null - and assetName = ?` + and assetName like 'https://utilityapi.com/DataCustodian/espi/1_1/resource/Subscription/${assetRow.utilityApiAuthorizationNumber}/%'` ) .run( assetCategory?.categoryId, assetRow.assetName.trim(), updateUser.userName, - Date.now(), - assetRow.accountNumberElectricity.toString() + Date.now() ) } + /* if ( assetRow.accountNumberGas !== undefined && assetRow.accountNumberGas !== '' @@ -98,7 +99,7 @@ function updateSsmPucAssetNames(): void { } if (assetId === undefined) { - assetId = addAsset( + assetId = await addAsset( { assetName: assetRow.assetName, categoryId: assetCategory?.categoryId @@ -118,9 +119,11 @@ function updateSsmPucAssetNames(): void { emileDB ) } + + */ } emileDB.close() } -updateSsmPucAssetNames() +await updateSsmPucAssetNames()