Refactored Export to CSV to send chunks of data + handle interruption from the front end
MODIFY
src/authorization/Authorizations.ts
src/authorization/Authorizations.ts
@@ -202,7 +202,7 @@ export default class Authorizations {
return Authorizations.canPerformAction(loggedUser, Entity.LOGGINGS, Action.LIST);
}
- public static canReadLogging(loggedUser: UserToken): boolean {
+ public static canReadLog(loggedUser: UserToken): boolean {
return Authorizations.canPerformAction(loggedUser, Entity.LOGGING, Action.READ);
}

MODIFY
src/server/rest/CentralRestServerService.ts
src/server/rest/CentralRestServerService.ts
@@ -108,9 +108,9 @@ class RequestMapper {
case 'GET':
// Register REST actions
this.registerJsonActionsPaths({
- [ServerAction.LOGGINGS]: LoggingService.handleGetLoggings.bind(this),
- [ServerAction.LOGGING]: LoggingService.handleGetLogging.bind(this),
- [ServerAction.LOGGINGS_EXPORT]: LoggingService.handleGetLoggingsExport.bind(this),
+ [ServerAction.LOGGINGS]: LoggingService.handleGetLogs.bind(this),
+ [ServerAction.LOGGING]: LoggingService.handleGetLog.bind(this),
+ [ServerAction.LOGGINGS_EXPORT]: LoggingService.handleExportLogs.bind(this),
[ServerAction.CHARGING_STATIONS]: ChargingStationService.handleGetChargingStations.bind(this),
[ServerAction.CAR_CATALOGS]: CarService.handleGetCarCatalogs.bind(this),
[ServerAction.CAR_CATALOG]: CarService.handleGetCarCatalog.bind(this),
@@ -119,8 +119,8 @@ class RequestMapper {
[ServerAction.CAR]: CarService.handleGetCar.bind(this),
[ServerAction.CAR_USERS]: CarService.handleGetCarUsers.bind(this),
[ServerAction.CAR_CATALOG_IMAGES]: CarService.handleGetCarCatalogImages.bind(this),
- [ServerAction.CHARGING_STATIONS_EXPORT]: ChargingStationService.handleGetChargingStationsExport.bind(this),
- [ServerAction.CHARGING_STATIONS_OCPP_PARAMS_EXPORT]: ChargingStationService.handleChargingStationsOCPPParamsExport.bind(this),
+ [ServerAction.CHARGING_STATIONS_EXPORT]: ChargingStationService.handleExportChargingStations.bind(this),
+ [ServerAction.CHARGING_STATIONS_OCPP_PARAMS_EXPORT]: ChargingStationService.handleExportChargingStationsOCPPParams.bind(this),
[ServerAction.CHARGING_STATION]: ChargingStationService.handleGetChargingStation.bind(this),
[ServerAction.CHECK_SMART_CHARGING_CONNECTION]: ChargingStationService.handleCheckSmartChargingConnection.bind(this),
[ServerAction.CHARGING_PROFILES]: ChargingStationService.handleGetChargingProfiles.bind(this),
@@ -159,9 +159,9 @@ class RequestMapper {
[ServerAction.TAG]: UserService.handleGetTag.bind(this),
[ServerAction.TRANSACTIONS_COMPLETED]: TransactionService.handleGetTransactionsCompleted.bind(this),
[ServerAction.TRANSACTIONS_TO_REFUND]: TransactionService.handleGetTransactionsToRefund.bind(this),
- [ServerAction.TRANSACTIONS_TO_REFUND_EXPORT]: TransactionService.handleGetTransactionsToRefundExport.bind(this),
+ [ServerAction.TRANSACTIONS_TO_REFUND_EXPORT]: TransactionService.handleExportTransactionsToRefund.bind(this),
[ServerAction.TRANSACTIONS_TO_REFUND_REPORTS]: TransactionService.handleGetRefundReports.bind(this),
- [ServerAction.TRANSACTIONS_EXPORT]: TransactionService.handleGetTransactionsExport.bind(this),
+ [ServerAction.TRANSACTIONS_EXPORT]: TransactionService.handleExportTransactions.bind(this),
[ServerAction.TRANSACTIONS_ACTIVE]: TransactionService.handleGetTransactionsActive.bind(this),
[ServerAction.TRANSACTIONS_IN_ERROR]: TransactionService.handleGetTransactionsInError.bind(this),
[ServerAction.TRANSACTION_YEARS]: TransactionService.handleGetTransactionYears.bind(this),
@@ -172,7 +172,7 @@ class RequestMapper {
[ServerAction.CHARGING_STATION_INACTIVITY_STATISTICS]: StatisticService.handleGetChargingStationInactivityStatistics.bind(this),
[ServerAction.CHARGING_STATION_TRANSACTIONS_STATISTICS]: StatisticService.handleGetChargingStationTransactionsStatistics.bind(this),
[ServerAction.CHARGING_STATION_PRICING_STATISTICS]: StatisticService.handleGetChargingStationPricingStatistics.bind(this),
- [ServerAction.STATISTICS_EXPORT]: StatisticService.handleGetStatisticsExport.bind(this),
+ [ServerAction.STATISTICS_EXPORT]: StatisticService.handleExportStatistics.bind(this),
[ServerAction.USER_CONSUMPTION_STATISTICS]: StatisticService.handleGetUserConsumptionStatistics.bind(this),
[ServerAction.USER_USAGE_STATISTICS]: StatisticService.handleGetUserUsageStatistics.bind(this),
[ServerAction.USER_INACTIVITY_STATISTICS]: StatisticService.handleGetUserInactivityStatistics.bind(this),

MODIFY
src/server/rest/service/ChargingStationService.ts
src/server/rest/service/ChargingStationService.ts
@@ -757,7 +757,7 @@ export default class ChargingStationService {
next();
}
- public static async handleChargingStationsOCPPParamsExport(action: ServerAction, req: Request, res: Response, next: NextFunction): Promise<void> {
+ public static async handleExportChargingStationsOCPPParams(action: ServerAction, req: Request, res: Response, next: NextFunction): Promise<void> {
// Always with site
req.query.WithSite = 'true';
// Get Charging Stations
@@ -771,61 +771,35 @@ export default class ChargingStationService {
action: Action.EXPORT_PARAMS,
entity: Entity.CHARGING_STATION,
module: MODULE_NAME,
- method: 'handleChargingStationsOCPPParamsExport',
+ method: 'handleExportChargingStationsOCPPParams',
});
}
}
const ocppParams: OCPPParams[] = [];
+ // Set the attachement name
+ res.attachment('exported-ocpp-params.csv');
+ let writeHeader = true;
for (const chargingStation of chargingStations.result) {
const ocppParameters = await ChargingStationStorage.getOcppParameters(req.user.tenantID, chargingStation.id);
// Get OCPP Params
- ocppParams.push({
+ const dataToExport = ChargingStationService.convertOCPPParamsToCSV({
params: ocppParameters.result,
siteName: chargingStation.siteArea.site.name,
siteAreaName: chargingStation.siteArea.name,
chargingStationName: chargingStation.id
- });
- }
- const dataToExport = ChargingStationService.convertOCPPParamsToCSV(ocppParams);
- // Build export
- const filename = 'exported-ocpp-params.csv';
- fs.writeFile(filename, dataToExport, (err) => {
- if (err) {
- throw err;
- }
- res.download(filename, (err2) => {
- if (err2) {
- throw err2;
+ }, writeHeader);
+ // Send OCPP Params
+ res.write(dataToExport);
+ writeHeader = false;
}
- fs.unlink(filename, (err3) => {
- if (err3) {
- throw err3;
- }
- });
- });
- });
+ // End of stream
+ res.end();
}
- public static async handleGetChargingStationsExport(action: ServerAction, req: Request, res: Response, next: NextFunction): Promise<void> {
- // Get Charging Stations
- const chargingStations = await ChargingStationService.getChargingStations(req);
- // Build export
- const filename = 'exported-charging-stations.csv';
- fs.writeFile(filename, ChargingStationService.convertToCSV(req.user, chargingStations.result), (err) => {
- if (err) {
- throw err;
- }
- res.download(filename, (err2) => {
- if (err2) {
- throw err2;
- }
- fs.unlink(filename, (err3) => {
- if (err3) {
- throw err3;
- }
- });
- });
- });
+ public static async handleExportChargingStations(action: ServerAction, req: Request, res: Response, next: NextFunction): Promise<void> {
+ // Export
+ await UtilsService.exportToCSV(req, res, 'exported-charging-stations.csv',
+ ChargingStationService.getChargingStations.bind(this), ChargingStationService.convertToCSV.bind(this));
}
public static async handleGetChargingStationsInError(action: ServerAction, req: Request, res: Response, next: NextFunction): Promise<void> {
@@ -1250,23 +1224,31 @@ export default class ChargingStationService {
return chargingStations;
}
- private static convertOCPPParamsToCSV(configurations: OCPPParams[]): string {
- let csv = `Charging Station${Constants.CSV_SEPARATOR}Name${Constants.CSV_SEPARATOR}Value${Constants.CSV_SEPARATOR}Site Area${Constants.CSV_SEPARATOR}Site\r\n`;
- for (const config of configurations) {
- for (const param of config.params) {
- csv += `${config.chargingStationName}` + Constants.CSV_SEPARATOR;
+ private static convertOCPPParamsToCSV(ocppParams: OCPPParams, writeHeader = true): string {
+ let csv = '';
+ // Header
+ if (writeHeader) {
+ csv = `Charging Station${Constants.CSV_SEPARATOR}Name${Constants.CSV_SEPARATOR}Value${Constants.CSV_SEPARATOR}Site Area${Constants.CSV_SEPARATOR}Site\r\n`;
+ }
+ // Content
+ for (const param of ocppParams.params) {
+ csv += `${ocppParams.chargingStationName}` + Constants.CSV_SEPARATOR;
csv += `${param.key}` + Constants.CSV_SEPARATOR;
csv += `${Utils.replaceSpecialCharsInCSVValueParam(param.value)}` + Constants.CSV_SEPARATOR;
- csv += `${config.siteAreaName}` + Constants.CSV_SEPARATOR;
- csv += `${config.siteName}\r\n`;
- }
+ csv += `${ocppParams.siteAreaName}` + Constants.CSV_SEPARATOR;
+ csv += `${ocppParams.siteName}\r\n`;
}
return csv;
}
- private static convertToCSV(loggedUser: UserToken, chargingStations: ChargingStation[]): string {
+ private static convertToCSV(loggedUser: UserToken, chargingStations: ChargingStation[], writeHeader = true): string {
+ let csv = '';
const i18nManager = new I18nManager(loggedUser.locale);
- let csv = `Name${Constants.CSV_SEPARATOR}Created On${Constants.CSV_SEPARATOR}Number of Connectors${Constants.CSV_SEPARATOR}Site Area${Constants.CSV_SEPARATOR}Latitude${Constants.CSV_SEPARATOR}Longitude${Constants.CSV_SEPARATOR}Charge Point S/N${Constants.CSV_SEPARATOR}Model${Constants.CSV_SEPARATOR}Charge Box S/N${Constants.CSV_SEPARATOR}Vendor${Constants.CSV_SEPARATOR}Firmware Version${Constants.CSV_SEPARATOR}OCPP Version${Constants.CSV_SEPARATOR}OCPP Protocol${Constants.CSV_SEPARATOR}Last Heartbeat${Constants.CSV_SEPARATOR}Last Reboot${Constants.CSV_SEPARATOR}Maximum Power (Watt)${Constants.CSV_SEPARATOR}Power Limit Unit\r\n`;
+ // Header
+ if (writeHeader) {
+ csv = `Name${Constants.CSV_SEPARATOR}Created On${Constants.CSV_SEPARATOR}Number of Connectors${Constants.CSV_SEPARATOR}Site Area${Constants.CSV_SEPARATOR}Latitude${Constants.CSV_SEPARATOR}Longitude${Constants.CSV_SEPARATOR}Charge Point S/N${Constants.CSV_SEPARATOR}Model${Constants.CSV_SEPARATOR}Charge Box S/N${Constants.CSV_SEPARATOR}Vendor${Constants.CSV_SEPARATOR}Firmware Version${Constants.CSV_SEPARATOR}OCPP Version${Constants.CSV_SEPARATOR}OCPP Protocol${Constants.CSV_SEPARATOR}Last Heartbeat${Constants.CSV_SEPARATOR}Last Reboot${Constants.CSV_SEPARATOR}Maximum Power (Watt)${Constants.CSV_SEPARATOR}Power Limit Unit\r\n`;
+ }
+ // Content
for (const chargingStation of chargingStations) {
csv += `${chargingStation.id}` + Constants.CSV_SEPARATOR;
csv += `${i18nManager.formatDateTime(chargingStation.createdOn, 'L')} ${i18nManager.formatDateTime(chargingStation.createdOn, 'LT')}` + Constants.CSV_SEPARATOR;

MODIFY
src/server/rest/service/LoggingService.ts
src/server/rest/service/LoggingService.ts
@@ -5,6 +5,7 @@ import AppAuthError from '../../../exception/AppAuthError';
import Authorizations from '../../../authorization/Authorizations';
import ChargingStationStorage from '../../../storage/mongodb/ChargingStationStorage';
import Constants from '../../../utils/Constants';
+import { DataResult } from '../../../types/DataResult';
import { HTTPAuthError } from '../../../types/HTTPError';
import I18nManager from '../../../utils/I18nManager';
import { Log } from '../../../types/Log';
@@ -14,26 +15,91 @@ import { ServerAction } from '../../../types/Server';
import TenantComponents from '../../../types/TenantComponents';
import UserToken from '../../../types/UserToken';
import Utils from '../../../utils/Utils';
+import UtilsService from './UtilsService';
import fs from 'fs';
+import moment from 'moment';
const MODULE_NAME = 'LoggingService';
export default class LoggingService {
- static async handleGetLoggings(action: ServerAction, req: Request, res: Response, next: NextFunction): Promise<void> {
+ public static async handleGetLogs(action: ServerAction, req: Request, res: Response, next: NextFunction): Promise<void> {
+ // Get logs
+ const loggings = await LoggingService.getLogs(req);
+ // Filter
+ LoggingSecurity.filterLogsResponse(loggings, req.user);
+ // Return
+ res.json(loggings);
+ next();
+ }
+
+ public static async handleExportLogs(action: ServerAction, req: Request, res: Response, next: NextFunction): Promise<void> {
+ // Export
+ await UtilsService.exportToCSV(req, res, 'exported-logs.csv',
+ LoggingService.getLogs.bind(this), LoggingService.convertToCSV.bind(this));
+ }
+
+ public static async handleGetLog(action: ServerAction, req: Request, res: Response, next: NextFunction): Promise<void> {
+ // Filter
+ const filteredRequest = LoggingSecurity.filterLogRequest(req.query);
+ // Get logs
+ const logging = await LoggingStorage.getLog(req.user.tenantID, filteredRequest.ID);
// Check auth
- if (!Authorizations.canListLoggings(req.user)) {
+ if (!Authorizations.canReadLog(req.user)) {
throw new AppAuthError({
errorCode: HTTPAuthError.ERROR,
user: req.user,
- action: Action.LIST,
- entity: Entity.LOGGINGS,
+ action: Action.READ,
+ entity: Entity.LOGGING,
module: MODULE_NAME,
- method: 'handleGetLoggings'
+ method: 'handleGetLog'
+ });
+ }
+ // Return
+ res.json(
+ LoggingSecurity.filterLogResponse(
+ logging, req.user, true
+ )
+ );
+ next();
+ }
+
+ private static convertToCSV(loggedUser: UserToken, loggings: Log[], writeHeader = true): string {
+ const i18nManager = new I18nManager(loggedUser.locale);
+ let csv = '';
+ // Header
+ if (writeHeader) {
+ csv = `Date${Constants.CSV_SEPARATOR}Time${Constants.CSV_SEPARATOR}Level${Constants.CSV_SEPARATOR}Type${Constants.CSV_SEPARATOR}Action${Constants.CSV_SEPARATOR}Message${Constants.CSV_SEPARATOR}Method${Constants.CSV_SEPARATOR}Module${Constants.CSV_SEPARATOR}Source${Constants.CSV_SEPARATOR}Host${Constants.CSV_SEPARATOR}Process\r\n`;
+ }
+ // Content
+ for (const log of loggings) {
+ csv += `${moment(log.timestamp).format('YYYY-MM-DD')}` + Constants.CSV_SEPARATOR;
+ csv += `${moment(log.timestamp).format('HH:mm:ss')}` + Constants.CSV_SEPARATOR;
+ csv += `${log.level}` + Constants.CSV_SEPARATOR;
+ csv += `${log.type}` + Constants.CSV_SEPARATOR;
+ csv += `${log.action}` + Constants.CSV_SEPARATOR;
+ csv += `${log.message}` + Constants.CSV_SEPARATOR;
+ csv += `${log.method}` + Constants.CSV_SEPARATOR;
+ csv += `${log.module}` + Constants.CSV_SEPARATOR;
+ csv += `${log.source}` + Constants.CSV_SEPARATOR;
+ csv += `${log.host}` + Constants.CSV_SEPARATOR;
+ csv += `${log.process}\r\n`;
+ }
+ return csv;
+ }
+
+ private static async getLogs(req: Request): Promise<DataResult<Log>> {
+ // Check auth
+ if (!Authorizations.canListLoggings(req.user)) {
+ throw new AppAuthError({
+ errorCode: HTTPAuthError.ERROR,
+ user: req.user,
+ action: Action.LIST, entity: Entity.LOGGINGS,
+ module: MODULE_NAME, method: 'getLogs'
});
}
// Filter
- const filteredRequest = LoggingSecurity.filterLoggingsRequest(req.query);
- // Check if organization component is active
+ const filteredRequest = LoggingSecurity.filterLogsRequest(req.query);
+ // Add filter for Site Admins
if (Utils.isComponentActiveFromToken(req.user, TenantComponents.ORGANIZATION) && Authorizations.isSiteAdmin(req.user)) {
// Optimization: Retrieve Charging Stations to get the logs only for the Site Admin user
const chargingStations = await ChargingStationStorage.getChargingStations(req.user.tenantID,
@@ -72,105 +138,7 @@ export default class LoggingService {
sort: filteredRequest.Sort,
onlyRecordCount: filteredRequest.OnlyRecordCount
});
- // Filter
- LoggingSecurity.filterLoggingsResponse(loggings, req.user);
- // Return
- res.json(loggings);
- next();
- }
-
- static async handleGetLoggingsExport(action: ServerAction, req: Request, res: Response, next: NextFunction): Promise<void> {
- // Check auth
- if (!Authorizations.canListLoggings(req.user)) {
- throw new AppAuthError({
- errorCode: HTTPAuthError.ERROR,
- user: req.user,
- action: Action.LIST,
- entity: Entity.LOGGINGS,
- module: MODULE_NAME,
- method: 'handleGetLoggingsExport'
- });
- }
- // Filter
- const filteredRequest = LoggingSecurity.filterLoggingsRequest(req.query);
- // Get logs
- const loggings = await LoggingStorage.getLogs(req.user.tenantID, {
- search: filteredRequest.Search,
- startDateTime: filteredRequest.StartDateTime,
- endDateTime: filteredRequest.EndDateTime,
- userIDs: filteredRequest.UserID ? filteredRequest.UserID.split('|') : null,
- hosts: filteredRequest.Host ? filteredRequest.Host.split('|') : null,
- levels: filteredRequest.Level ? filteredRequest.Level.split('|') : null,
- type: filteredRequest.Type,
- sources: filteredRequest.Source ? filteredRequest.Source.split('|') : null,
- actions: filteredRequest.Action ? filteredRequest.Action.split('|') : null,
- }, {
- limit: filteredRequest.Limit,
- skip: filteredRequest.Skip,
- sort: filteredRequest.Sort,
- onlyRecordCount: filteredRequest.OnlyRecordCount
- });
- // Filter
- LoggingSecurity.filterLoggingsResponse(loggings, req.user);
- const filename = 'exported-logs.csv';
- fs.writeFile(filename, LoggingService.convertToCSV(req.user, loggings.result), (err) => {
- if (err) {
- throw err;
- }
- res.download(filename, (err2) => {
- if (err2) {
- throw err2;
- }
- fs.unlink(filename, (err3) => {
- if (err3) {
- throw err3;
- }
- });
- });
- });
- }
-
- static async handleGetLogging(action: ServerAction, req: Request, res: Response, next: NextFunction): Promise<void> {
- // Filter
- const filteredRequest = LoggingSecurity.filterLoggingRequest(req.query);
- // Get logs
- const logging = await LoggingStorage.getLog(req.user.tenantID, filteredRequest.ID);
- // Check auth
- if (!Authorizations.canReadLogging(req.user)) {
- throw new AppAuthError({
- errorCode: HTTPAuthError.ERROR,
- user: req.user,
- action: Action.READ,
- entity: Entity.LOGGING,
- module: MODULE_NAME,
- method: 'handleGetLogging'
- });
- }
- // Return
- res.json(
- LoggingSecurity.filterLoggingResponse(
- logging, req.user, true
- )
- );
- next();
- }
-
- private static convertToCSV(loggedUser: UserToken, loggings: Log[]): string {
- const i18nManager = new I18nManager(loggedUser.locale);
- let csv = `Date${Constants.CSV_SEPARATOR}Level${Constants.CSV_SEPARATOR}Type${Constants.CSV_SEPARATOR}Action${Constants.CSV_SEPARATOR}Message${Constants.CSV_SEPARATOR}Method${Constants.CSV_SEPARATOR}Module${Constants.CSV_SEPARATOR}Source${Constants.CSV_SEPARATOR}Host${Constants.CSV_SEPARATOR}Process\r\n`;
- for (const log of loggings) {
- csv += `${i18nManager.formatDateTime(log.timestamp, 'L')} ${i18nManager.formatDateTime(log.timestamp, 'LT')}` + Constants.CSV_SEPARATOR;
- csv += `${log.level}` + Constants.CSV_SEPARATOR;
- csv += `${log.type}` + Constants.CSV_SEPARATOR;
- csv += `${log.action}` + Constants.CSV_SEPARATOR;
- csv += `${log.message}` + Constants.CSV_SEPARATOR;
- csv += `${log.method}` + Constants.CSV_SEPARATOR;
- csv += `${log.module}` + Constants.CSV_SEPARATOR;
- csv += `${log.source}` + Constants.CSV_SEPARATOR;
- csv += `${log.host}` + Constants.CSV_SEPARATOR;
- csv += `${log.process}\r\n`;
- }
- return csv;
+ return loggings;
}
}

MODIFY
src/server/rest/service/StatisticService.ts
src/server/rest/service/StatisticService.ts
@@ -311,10 +311,10 @@ export default class StatisticService {
next();
}
- static async handleGetStatisticsExport(action: ServerAction, req: Request, res: Response, next: NextFunction) {
+ static async handleExportStatistics(action: ServerAction, req: Request, res: Response, next: NextFunction) {
// Check if component is active
UtilsService.assertComponentIsActiveFromToken(req.user, TenantComponents.STATISTICS,
- Action.LIST, Entity.TRANSACTIONS, MODULE_NAME, 'handleGetStatisticsExport');
+ Action.LIST, Entity.TRANSACTIONS, MODULE_NAME, 'handleExportStatistics');
// Check auth
if (!Authorizations.canListTransactions(req.user)) {
throw new AppAuthError({
@@ -323,7 +323,7 @@ export default class StatisticService {
action: Action.LIST,
entity: Entity.TRANSACTIONS,
module: MODULE_NAME,
- method: 'handleGetStatisticsExport'
+ method: 'handleExportStatistics'
});
}
// Filter
@@ -359,24 +359,15 @@ export default class StatisticService {
}
// Query data
const transactionStatsMDB = await StatisticsStorage[method](req.user.tenantID, filter, groupBy);
+ // Set the attachement name
+ res.attachment('exported-' + filteredRequest.DataType.toLowerCase() + '-statistics.csv');
// Build the result
- const filename = 'exported-' + filteredRequest.DataType.toLowerCase() + '-statistics.csv';
- fs.writeFile(filename, StatisticService.convertToCSV(req.user, transactionStatsMDB, filteredRequest.DataCategory,
- filteredRequest.DataType, filteredRequest.Year, filteredRequest.DataScope), (createError) => {
- if (createError) {
- throw createError;
- }
- res.download(filename, (downloadError) => {
- if (downloadError) {
- throw downloadError;
- }
- fs.unlink(filename, (unlinkError) => {
- if (unlinkError) {
- throw unlinkError;
- }
- });
- });
- });
+ const dataToExport = StatisticService.convertToCSV(req.user, transactionStatsMDB, filteredRequest.DataCategory,
+ filteredRequest.DataType, filteredRequest.Year, filteredRequest.DataScope);
+ // Send
+ res.write(dataToExport);
+ // End of stream
+ res.end();
}
// Only completed transactions

MODIFY
src/server/rest/service/TransactionService.ts
src/server/rest/service/TransactionService.ts
@@ -620,48 +620,16 @@ export default class TransactionService {
next();
}
- public static async handleGetTransactionsExport(action: ServerAction, req: Request, res: Response, next: NextFunction): Promise<void> {
- // Get transactions
- const transactions = await TransactionService.getTransactions(req);
- // Create the file
- const filename = 'exported-transactions.csv';
- fs.writeFile(filename, TransactionService.convertToCSV(req.user, transactions.result), (err) => {
- if (err) {
- throw err;
- }
- res.download(filename, (err2) => {
- if (err2) {
- throw err2;
- }
- fs.unlink(filename, (err3) => {
- if (err3) {
- throw err3;
- }
- });
- });
- });
+ public static async handleExportTransactions(action: ServerAction, req: Request, res: Response, next: NextFunction): Promise<void> {
+ // Export
+ await UtilsService.exportToCSV(req, res, 'exported-sessions.csv',
+ TransactionService.getTransactions.bind(this), TransactionService.convertToCSV.bind(this));
}
- public static async handleGetTransactionsToRefundExport(action: ServerAction, req: Request, res: Response, next: NextFunction): Promise<void> {
- // Get transactions
- const transactions = await TransactionService.getTransactions(req);
- // Create the file
- const filename = 'exported-refund-transactions.csv';
- fs.writeFile(filename, TransactionService.convertToCSV(req.user, transactions.result), (err) => {
- if (err) {
- throw err;
- }
- res.download(filename, (err2) => {
- if (err2) {
- throw err2;
- }
- fs.unlink(filename, (err3) => {
- if (err3) {
- throw err3;
- }
- });
- });
- });
+ public static async handleExportTransactionsToRefund(action: ServerAction, req: Request, res: Response, next: NextFunction): Promise<void> {
+ // Export
+ await UtilsService.exportToCSV(req, res, 'exported-refund-sessions.csv',
+ TransactionService.getTransactions.bind(this), TransactionService.convertToCSV.bind(this));
}
public static async handleGetTransactionsInError(action: ServerAction, req: Request, res: Response, next: NextFunction): Promise<void> {
@@ -732,10 +700,13 @@ export default class TransactionService {
next();
}
- public static convertToCSV(loggedUser: UserToken, transactions: Transaction[]): string {
+ public static convertToCSV(loggedUser: UserToken, transactions: Transaction[], writeHeader = true): string {
const i18nManager = new I18nManager(loggedUser.locale);
- // Headers
- let csv = `ID${Constants.CSV_SEPARATOR}Charging Station${Constants.CSV_SEPARATOR}Connector${Constants.CSV_SEPARATOR}User ID${Constants.CSV_SEPARATOR}User${Constants.CSV_SEPARATOR}Start Date${Constants.CSV_SEPARATOR}End Date${Constants.CSV_SEPARATOR}Total Consumption (kW.h)${Constants.CSV_SEPARATOR}Total Duration (Mins)${Constants.CSV_SEPARATOR}Total Inactivity (Mins)${Constants.CSV_SEPARATOR}Price${Constants.CSV_SEPARATOR}Price Unit\r\n`;
+ let csv = '';
+ // Header
+ if (writeHeader) {
+ csv = `ID${Constants.CSV_SEPARATOR}Charging Station${Constants.CSV_SEPARATOR}Connector${Constants.CSV_SEPARATOR}User ID${Constants.CSV_SEPARATOR}User${Constants.CSV_SEPARATOR}Start Date${Constants.CSV_SEPARATOR}Start Time${Constants.CSV_SEPARATOR}End Date${Constants.CSV_SEPARATOR}End Time${Constants.CSV_SEPARATOR}Total Consumption (kW.h)${Constants.CSV_SEPARATOR}Total Duration (Mins)${Constants.CSV_SEPARATOR}Total Inactivity (Mins)${Constants.CSV_SEPARATOR}Price${Constants.CSV_SEPARATOR}Price Unit\r\n`;
+ }
// Content
for (const transaction of transactions) {
csv += `${transaction.id}` + Constants.CSV_SEPARATOR;
@@ -743,8 +714,10 @@ export default class TransactionService {
csv += `${transaction.connectorId}` + Constants.CSV_SEPARATOR;
csv += `${transaction.user ? Cypher.hash(transaction.user.id) : ''}` + Constants.CSV_SEPARATOR;
csv += `${transaction.user ? Utils.buildUserFullName(transaction.user, false) : ''}` + Constants.CSV_SEPARATOR;
- csv += `${i18nManager.formatDateTime(transaction.timestamp, 'L')} ${i18nManager.formatDateTime(transaction.timestamp, 'LT')}` + Constants.CSV_SEPARATOR;
- csv += `${transaction.stop ? `${i18nManager.formatDateTime(transaction.stop.timestamp, 'L')} ${i18nManager.formatDateTime(transaction.stop.timestamp, 'LT')}` : ''}` + Constants.CSV_SEPARATOR;
+ csv += `${moment(transaction.timestamp).format('YYYY-MM-DD')}` + Constants.CSV_SEPARATOR;
+ csv += `${moment(transaction.timestamp).format('HH:mm:ss')}` + Constants.CSV_SEPARATOR;
+ csv += `${transaction.stop ? `${moment(transaction.stop.timestamp).format('YYYY-MM-DD')}` : ''}` + Constants.CSV_SEPARATOR;
+ csv += `${transaction.stop ? `${moment(transaction.stop.timestamp).format('HH:mm:ss')}` : ''}` + Constants.CSV_SEPARATOR;
csv += `${transaction.stop ? Math.round(transaction.stop.totalConsumptionWh ? transaction.stop.totalConsumptionWh / 1000 : 0) : ''}` + Constants.CSV_SEPARATOR;
csv += `${transaction.stop ? Math.round(transaction.stop.totalDurationSecs ? transaction.stop.totalDurationSecs / 60 : 0) : ''}` + Constants.CSV_SEPARATOR;
csv += `${transaction.stop ? Math.round(transaction.stop.totalInactivitySecs ? transaction.stop.totalInactivitySecs / 60 : 0) : ''}` + Constants.CSV_SEPARATOR;

MODIFY
src/server/rest/service/UtilsService.ts
src/server/rest/service/UtilsService.ts
@@ -5,6 +5,7 @@ import { NextFunction, Request, Response } from 'express';
import AppAuthError from '../../../exception/AppAuthError';
import AppError from '../../../exception/AppError';
import Constants from '../../../utils/Constants';
+import { DataResult } from '../../../types/DataResult';
import Logging from '../../../utils/Logging';
import { ServerAction } from '../../../types/Server';
import TenantComponents from '../../../types/TenantComponents';
@@ -71,4 +72,40 @@ export default class UtilsService {
});
}
}
+
+ public static async exportToCSV(req: Request, res: Response, attachementName: string,
+ handleGetData: (req: Request) => Promise<DataResult<any>>,
+ handleConvertToCSV: (loggedUser: UserToken, data: any[], writeHeader: boolean) => string) {
+ // Override
+ req.query.Limit = Constants.EXPORT_PAGE_SIZE.toString();
+ // Set the attachement name
+ res.attachment(attachementName);
+ // Get the total number of Logs
+ req.query.OnlyRecordCount = 'true';
+ let data = await handleGetData(req);
+ const count = data.count;
+ delete req.query.OnlyRecordCount;
+ let skip = 0;
+ // Handle closed socket
+ let connectionClosed = false;
+ req.connection.on('close', () => {
+ connectionClosed = true;
+ });
+ do {
+ // Check if the socket is closed and stop the process
+ if (connectionClosed) {
+ break;
+ }
+ console.log(skip);
+ // Get the Logs
+ req.query.Skip = skip.toString();
+ data = await handleGetData(req);
+ // Send Transactions
+ res.write(handleConvertToCSV(req.user, data.result, (skip === 0)));
+ // Next page
+ skip += Constants.EXPORT_PAGE_SIZE;
+ } while (skip < count);
+ // End of stream
+ res.end();
+ }
}

MODIFY
src/server/rest/service/security/LoggingSecurity.ts
src/server/rest/service/security/LoggingSecurity.ts
@@ -9,7 +9,7 @@ import UtilsSecurity from './UtilsSecurity';
import sanitize from 'mongo-sanitize';
export default class LoggingSecurity {
- static filterLoggingsRequest(request: any): HttpLogsRequest {
+ static filterLogsRequest(request: any): HttpLogsRequest {
const filteredRequest = {} as HttpLogsRequest;
// Get logs
filteredRequest.StartDateTime = sanitize(request.StartDateTime);
@@ -27,19 +27,19 @@ export default class LoggingSecurity {
return filteredRequest;
}
- static filterLoggingRequest(request: any): HttpByIDRequest {
+ static filterLogRequest(request: any): HttpByIDRequest {
return {
ID: sanitize(request.ID)
};
}
- static filterLoggingResponse(logging: Log, loggedUser: UserToken, withDetailedMessage = false): Log {
+ static filterLogResponse(logging: Log, loggedUser: UserToken, withDetailedMessage = false): Log {
const filteredLogging: Log = {} as Log;
if (!logging) {
return null;
}
- if (!Authorizations.canReadLogging(loggedUser)) {
+ if (!Authorizations.canReadLog(loggedUser)) {
return null;
}
filteredLogging.id = logging.id;
@@ -69,14 +69,14 @@ export default class LoggingSecurity {
return filteredLogging;
}
- static filterLoggingsResponse(logs: DataResult<Log>, loggedUser: UserToken): void {
+ static filterLogsResponse(logs: DataResult<Log>, loggedUser: UserToken): void {
const filteredLogs = [];
if (!logs.result) {
return null;
}
for (const logging of logs.result) {
// Filter
- const filteredLogging = LoggingSecurity.filterLoggingResponse(logging, loggedUser);
+ const filteredLogging = LoggingSecurity.filterLogResponse(logging, loggedUser);
// Ok?
if (filteredLogging) {
// Add

MODIFY
src/utils/Constants.ts
src/utils/Constants.ts
@@ -12,11 +12,14 @@ export default class Constants {
public static readonly DB_RECORD_COUNT_DEFAULT = 100;
public static readonly DB_RECORD_COUNT_CEIL = 2000;
public static readonly DB_RECORD_COUNT_NO_LIMIT = Number.MAX_SAFE_INTEGER;
+ public static readonly DB_UNDETERMINED_NBR_OF_RECORDS = -1;
public static readonly DB_PARAMS_MAX_LIMIT: DbParams = Object.freeze({ limit: Constants.DB_RECORD_COUNT_NO_LIMIT, skip: 0, sort: null });
public static readonly DB_PARAMS_SINGLE_RECORD: DbParams = Object.freeze({ limit: 1, skip: 0, sort: null });
public static readonly DB_PARAMS_COUNT_ONLY: DbParams = Object.freeze({ limit: Constants.DB_RECORD_COUNT_NO_LIMIT, skip: 0, onlyRecordCount: true, sort: null });
+ public static readonly EXPORT_PAGE_SIZE = 1000;
+
public static readonly DEFAULT_TENANT = 'default';
public static readonly DEFAULT_TENANT_OBJECT= {
id: Constants.DEFAULT_TENANT,

Refactored Export to CSV to send chunks of data + handle interruption from the front end