diff --git a/apps/drec-api/src/models/AggregateMetervalue.ts b/apps/drec-api/src/models/AggregateMetervalue.ts index 49ad52ed6..329e965e9 100755 --- a/apps/drec-api/src/models/AggregateMetervalue.ts +++ b/apps/drec-api/src/models/AggregateMetervalue.ts @@ -1,13 +1,18 @@ import { Unit } from '@energyweb/energy-api-influxdb'; -import { IsDate, IsOptional, IsPositive } from 'class-validator'; +import { IsNotEmpty, IsOptional, IsPositive } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; +import { IsTimestamp } from '../validations/timestamp'; +import { ConvertToNullIfEmpty } from '../transformers/string'; export class NewReadDTO { @ApiProperty({ type: Date }) + @ConvertToNullIfEmpty() @IsOptional() + @IsTimestamp() starttimestamp: Date; @ApiProperty({ type: Date }) - @IsDate() + @IsNotEmpty() + @IsTimestamp() endtimestamp: Date; @ApiProperty({ type: Number }) diff --git a/apps/drec-api/src/pods/reads/dto/intermediate_meter_read.dto.ts b/apps/drec-api/src/pods/reads/dto/intermediate_meter_read.dto.ts index b750c8c9a..a3f0257a2 100755 --- a/apps/drec-api/src/pods/reads/dto/intermediate_meter_read.dto.ts +++ b/apps/drec-api/src/pods/reads/dto/intermediate_meter_read.dto.ts @@ -12,8 +12,10 @@ import { ReadType } from '../../../utils/enums'; import { Iintermediate, NewReadDTO } from '../../../models'; import { PrimaryGeneratedColumn, Column } from 'typeorm'; import { IsValidTimezone } from '../../../validations/timezone'; -import { Transform } from 'class-transformer'; +import { Transform, Type } from 'class-transformer'; import { transformTimezone } from '../../../transformers/timezone'; +import { Trim } from '../../../transformers/string'; + export class IntmediateMeterReadDTO implements Omit { @ApiProperty({ type: Number }) @PrimaryGeneratedColumn() @@ -46,6 +48,7 @@ export class NewIntmediateMeterReadDTO @ApiProperty() @IsString() @IsOptional() + @Trim() @IsValidTimezone() @Transform(transformTimezone) timezone?: string; @@ -61,6 +64,7 @@ export class NewIntmediateMeterReadDTO @ApiProperty({ type: () => [NewReadDTO] }) @IsArray() @ValidateNested() + @Type(() => NewReadDTO) reads: NewReadDTO[]; @ApiProperty({ type: () => Number }) diff --git a/apps/drec-api/src/pods/reads/reads.controller.ts b/apps/drec-api/src/pods/reads/reads.controller.ts index 56b424cb1..1c2b592dc 100644 --- a/apps/drec-api/src/pods/reads/reads.controller.ts +++ b/apps/drec-api/src/pods/reads/reads.controller.ts @@ -41,7 +41,10 @@ import { Permission } from '../permission/decorators/permission.decorator'; import { ACLModules } from '../access-control-layer-module-service/decorator/aclModule.decorator'; import { OrganizationService } from '../organization/organization.service'; import { UserService } from '../user/user.service'; -` `; +import { + toTimezoneDate, + toTimezoneDateFormat, +} from '../../transformers/timezone'; @Controller('meter-reads') @ApiBearerAuth('access-token') @@ -367,125 +370,23 @@ export class ReadsController extends BaseReadsController { }); } - if ( - measurements.timezone !== null && - measurements.timezone !== undefined && - measurements.timezone.toString().trim() !== '' - ) { - measurements.timezone = measurements.timezone.toString().trim(); - - let dateInvalid = false; - measurements.reads.forEach((ele) => { - for (const key in ele) { - if (key === 'starttimestamp' || key === 'endtimestamp') { - if (ele[key]) { - const dateTimeRegex = - /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.{0,1}\d{0,3}$/; - if (ele[key].toString().includes('.')) { - if ( - Number.isNaN( - parseFloat( - ele[key] - .toString() - .substring( - ele[key].toString().indexOf('.'), - ele[key].toString().length, - ), - ), - ) - ) { - this.logger.error( - `Invalid date sent ${ele[key]}` + - ` please sent valid date, format for dates is YYYY-MM-DD hh:mm:ss example 2020-02-19 19:20:55 or to include milliseconds add dot and upto 3 digits after seconds example 2020-02-19 19:20:55.2 or 2020-02-19 19:20:54.333`, - ); - throw new ConflictException({ - success: false, - message: - `Invalid date sent ${ele[key]}` + - ` please sent valid date, format for dates is YYYY-MM-DD hh:mm:ss example 2020-02-19 19:20:55 or to include milliseconds add dot and upto 3 digits after seconds example 2020-02-19 19:20:55.2 or 2020-02-19 19:20:54.333`, - }); - } - } - - if (!dateTimeRegex.test(ele[key].toString())) { - dateInvalid = true; - this.logger.error( - `Invalid date sent ${ele[key]}` + - ` please sent valid date, format for dates is YYYY-MM-DD hh:mm:ss example 2020-02-19 19:20:55 or to include milliseconds add dot and upto 3 digits after seconds example 2020-02-19 19:20:55.2 or 2020-02-19 19:20:54.333`, - ); - throw new ConflictException({ - success: false, - message: - `Invalid date sent ${ele[key]}` + - ` please sent valid date, format for dates is YYYY-MM-DD hh:mm:ss example 2020-02-19 19:20:55 or to include milliseconds add dot and upto 3 digits after seconds example 2020-02-19 19:20:55.2 or 2020-02-19 19:20:54.333`, - }); - } else { - const dateTime = momentTimeZone.tz( - ele[key], - measurements.timezone, - ); - if (!dateTime.isValid()) { - dateInvalid = true; - this.logger.error(`Invalid date sent ${ele[key]}`); - throw new ConflictException({ - success: false, - message: `Invalid date sent ${ele[key]}`, - }); - } else { - let milliSeondsToAddSentInRequest = ''; - if ( - ele[key].toString().includes('.') && - !isNaN( - parseInt( - ele[key] - .toString() - .substring( - ele[key].toString().indexOf('.'), - ele[key].toString().length, - ), - ), - ) - ) { - milliSeondsToAddSentInRequest = ele[key] - .toString() - .substring( - ele[key].toString().indexOf('.'), - ele[key].toString().length, - ); - } - let utcString: string = dateTime.clone().utc().format(); - - if (milliSeondsToAddSentInRequest != '') { - utcString = - utcString.substring(0, utcString.length - 1) + - milliSeondsToAddSentInRequest + - 'Z'; - } else { - utcString = - utcString.substring(0, utcString.length - 1) + '.000Z'; - } - ele[key] = new Date(utcString); - } - } - } - } - } - }); - if (dateInvalid) { - this.logger.error( - `Invalid date please sent valid date, format for dates is YYYY-MM-DD hh:mm:ss example 2020-02-19 19:20:55 or to include milliseconds add dot and upto 3 digits after seconds example 2020-02-19 19:20:55.2 or 2020-02-19 19:20:54.333`, - ); - throw new ConflictException({ - success: false, - message: `Invalid date please sent valid date, format for dates is YYYY-MM-DD hh:mm:ss example 2020-02-19 19:20:55 or to include milliseconds add dot and upto 3 digits after seconds example 2020-02-19 19:20:55.2 or 2020-02-19 19:20:54.333`, - }); - } - device.createdAt = momentTimeZone - .tz(device.createdAt, measurements.timezone) - .toDate(); - device.commissioningDate = momentTimeZone - .tz(new Date(device?.commissioningDate), measurements.timezone) - .format(); + if (measurements.timezone) { + measurements.reads = measurements.reads.map((read) => ({ + ...read, + starttimestamp: toTimezoneDate( + read.starttimestamp, + measurements.timezone, + ), + endtimestamp: toTimezoneDate(read.endtimestamp, measurements.timezone), + })); + device.createdAt = toTimezoneDate( + device.createdAt, + measurements.timezone, + ); + device.commissioningDate = toTimezoneDateFormat( + device?.commissioningDate || new Date(), + measurements.timezone, + ); } //check for according to read type if start time stamp and end time stamps are sent @@ -498,16 +399,7 @@ export class ReadsController extends BaseReadsController { let historyallStartDatesAreAftercommissioningDate = true; let historyallEndDatesAreAftercommissioningDate = true; measurements.reads.forEach((ele) => { - if ( - (ele.starttimestamp instanceof Date && - (ele.starttimestamp === null || - ele.starttimestamp === undefined || - isNaN(ele.starttimestamp.getTime()))) || - (ele.endtimestamp instanceof Date && - (ele.endtimestamp === null || - ele.endtimestamp === undefined || - isNaN(ele.endtimestamp.getTime()))) - ) { + if (!ele.starttimestamp || !ele.endtimestamp) { datesContainingNullOrEmptyValues = true; } const startdateformate = isValidUTCDateFormat( @@ -853,112 +745,18 @@ export class ReadsController extends BaseReadsController { measurements.timezone.toString().trim() !== '' ) { measurements.timezone = measurements.timezone.toString().trim(); - - let dateInvalid = false; - measurements.reads.forEach((ele) => { - for (const key in ele) { - if (key === 'starttimestamp' || key === 'endtimestamp') { - if (ele[key]) { - const dateTimeRegex = - /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.{0,1}\d{0,3}$/; - if (ele[key].toString().includes('.')) { - if ( - Number.isNaN( - parseFloat( - ele[key] - .toString() - .substring( - ele[key].toString().indexOf('.'), - ele[key].toString().length, - ), - ), - ) - ) { - this.logger.error( - `Invalid date sent ${ele[key]}` + - ` please sent valid date, format for dates is YYYY-MM-DD hh:mm:ss example 2020-02-19 19:20:55 or to include milliseconds add dot and upto 3 digits after seconds example 2020-02-19 19:20:55.2 or 2020-02-19 19:20:54.333`, - ); - throw new ConflictException({ - success: false, - message: - `Invalid date sent ${ele[key]}` + - ` please sent valid date, format for dates is YYYY-MM-DD hh:mm:ss example 2020-02-19 19:20:55 or to include milliseconds add dot and upto 3 digits after seconds example 2020-02-19 19:20:55.2 or 2020-02-19 19:20:54.333`, - }); - } - } - - if (!dateTimeRegex.test(ele[key].toString())) { - dateInvalid = true; - this.logger.error( - `Invalid date sent ${ele[key]}` + - ` please sent valid date, format for dates is YYYY-MM-DD hh:mm:ss example 2020-02-19 19:20:55 or to include milliseconds add dot and upto 3 digits after seconds example 2020-02-19 19:20:55.2 or 2020-02-19 19:20:54.333`, - ); - throw new ConflictException({ - success: false, - message: - `Invalid date sent ${ele[key]}` + - ` please sent valid date, format for dates is YYYY-MM-DD hh:mm:ss example 2020-02-19 19:20:55 or to include milliseconds add dot and upto 3 digits after seconds example 2020-02-19 19:20:55.2 or 2020-02-19 19:20:54.333`, - }); - } else { - const dateTime = momentTimeZone.tz( - ele[key], - measurements.timezone, - ); - if (!dateTime.isValid()) { - this.logger.error(`Invalid date sent ${ele[key]}`); - dateInvalid = true; - throw new ConflictException({ - success: false, - message: `Invalid date sent ${ele[key]}`, - }); - } else { - let milliSeondsToAddSentInRequest = ''; - if ( - ele[key].toString().includes('.') && - !isNaN( - parseInt( - ele[key] - .toString() - .substring( - ele[key].toString().indexOf('.'), - ele[key].toString().length, - ), - ), - ) - ) { - milliSeondsToAddSentInRequest = ele[key] - .toString() - .substring( - ele[key].toString().indexOf('.'), - ele[key].toString().length, - ); - } - let utcString: string = dateTime.clone().utc().format(); - - if (milliSeondsToAddSentInRequest != '') { - utcString = - utcString.substring(0, utcString.length - 1) + - milliSeondsToAddSentInRequest + - 'Z'; - } else { - utcString = - utcString.substring(0, utcString.length - 1) + '.000Z'; - } - ele[key] = new Date(utcString); - } - } - } - } - } - }); - if (dateInvalid) { - this.logger.error( - `Invalid date please sent valid date, format for dates is YYYY-MM-DD hh:mm:ss example 2020-02-19 19:20:55 or to include milliseconds add dot and upto 3 digits after seconds example 2020-02-19 19:20:55.2 or 2020-02-19 19:20:54.333`, - ); - throw new ConflictException({ - success: false, - message: `Invalid date please sent valid date, format for dates is YYYY-MM-DD hh:mm:ss example 2020-02-19 19:20:55 or to include milliseconds add dot and upto 3 digits after seconds example 2020-02-19 19:20:55.2 or 2020-02-19 19:20:54.333`, - }); + if (measurements.timezone) { + measurements.reads = measurements.reads.map((read) => ({ + ...read, + starttimestamp: toTimezoneDate( + read.starttimestamp, + measurements.timezone, + ), + endtimestamp: toTimezoneDate( + read.endtimestamp, + measurements.timezone, + ), + })); } device.createdAt = momentTimeZone .tz(device.createdAt, measurements.timezone) @@ -971,36 +769,15 @@ export class ReadsController extends BaseReadsController { //check for according to read type if start time stamp and end time stamps are sent if (measurements.type === ReadType.History) { let datesContainingNullOrEmptyValues = false; - let datevalid = true; let allDatesAreBeforeCreatedAt = true; let allStartDatesAreBeforeEnddate = true; let readvalue = true; let historyallStartDatesAreAftercommissioningDate = true; let historyallEndDatesAreAftercommissioningDate = true; measurements.reads.forEach((ele) => { - if ( - (ele.starttimestamp instanceof Date && - (ele.starttimestamp === null || - ele.starttimestamp === undefined || - isNaN(ele.starttimestamp.getTime()))) || - (ele.endtimestamp instanceof Date && - (ele.endtimestamp === null || - ele.endtimestamp === undefined || - isNaN(ele.endtimestamp.getTime()))) - ) { + if (!ele.starttimestamp || !ele.endtimestamp) { datesContainingNullOrEmptyValues = true; } - const startdateformate = isValidUTCDateFormat( - new Date(ele.starttimestamp).toISOString(), - ); - - const enddateformate = isValidUTCDateFormat( - new Date(ele.endtimestamp).toISOString(), - ); - - if (!startdateformate || !enddateformate) { - datevalid = false; - } if (device && device.createdAt) { if ( new Date(ele.endtimestamp).getTime() > @@ -1051,16 +828,7 @@ export class ReadsController extends BaseReadsController { 'One ore more Start Date and End Date values are not sent for History, start and end date is required for History meter ready type', }); } - if (!datevalid) { - this.logger.error( - `Invalid Start Date and/or End Date, valid format is YYYY-MM-DDThh:mm:ss.millisecondsZ example 2022-10-18T11:35:27.640Z`, - ); - throw new ConflictException({ - success: false, - message: - ' Invalid Start Date and/or End Date, valid format is YYYY-MM-DDThh:mm:ss.millisecondsZ example 2022-10-18T11:35:27.640Z ', - }); - } + if (!allStartDatesAreBeforeEnddate) { this.logger.error( `starttimestamp should be prior to endtimestamp. One or more measurements starttimestamp is greater than endtimestamp`, @@ -1120,12 +888,8 @@ export class ReadsController extends BaseReadsController { let currentdate: Date = new Date(); measurements.reads.forEach((ele) => { this.logger.log('Line No: 512'); - if ( - ele.endtimestamp instanceof Date && - (ele.endtimestamp === null || - ele.endtimestamp === undefined || - isNaN(ele.endtimestamp.getTime())) - ) { + + if (!ele.endtimestamp) { datesContainingNullOrEmptyValues = true; } const enddateformate = isValidUTCDateFormat( @@ -1164,11 +928,11 @@ export class ReadsController extends BaseReadsController { }); if (datesContainingNullOrEmptyValues) { this.logger.error( - `One ore more End Date values are not sent for ${measurements.type}, end date is required`, + `End Date values are not sent for ${measurements.type}, end date is required`, ); throw new ConflictException({ success: false, - message: `One ore more End Date values are not sent for ${measurements.type}, end date is required`, + message: `End Date values are not sent for ${measurements.type}, end date is required`, }); } if (!datevalid1) { diff --git a/apps/drec-api/src/transformers/string.ts b/apps/drec-api/src/transformers/string.ts index 8895c434e..8cea81426 100644 --- a/apps/drec-api/src/transformers/string.ts +++ b/apps/drec-api/src/transformers/string.ts @@ -1,5 +1,15 @@ import { applyDecorators } from '@nestjs/common'; import { Transform } from 'class-transformer'; +import { isNotEmpty } from 'class-validator'; export const Trim = (): PropertyDecorator => applyDecorators(Transform((value?: string) => value?.trim())); + +export const ConvertToNullIfEmpty = (): PropertyDecorator => + applyDecorators( + Trim(), + Transform((value?: string) => { + if (!isNotEmpty(value)) return null; + return value; + }), + ); diff --git a/apps/drec-api/src/transformers/timezone.ts b/apps/drec-api/src/transformers/timezone.ts index 4f56e281c..c0d292495 100644 --- a/apps/drec-api/src/transformers/timezone.ts +++ b/apps/drec-api/src/transformers/timezone.ts @@ -1,4 +1,5 @@ import * as momentTimezone from 'moment-timezone'; +import * as momentTimeZone from 'moment-timezone'; export const transformTimezone = (value?: string): string | null => { if (!value) return value; @@ -8,3 +9,21 @@ export const transformTimezone = (value?: string): string | null => { ); return index >= 0 ? allTimezones[index] : value; }; + +export const toTimezoneDate = ( + date: string | Date | null | undefined, + timezone: string, +): Date | null => { + if (!date) return null; + + return momentTimeZone.tz(date, timezone).toDate(); +}; + +export const toTimezoneDateFormat = ( + date: string | Date | null | undefined, + timezone: string, +): string | null => { + if (!date) return null; + + return momentTimeZone.tz(date, timezone).format(); +}; diff --git a/apps/drec-api/src/validations/timestamp.ts b/apps/drec-api/src/validations/timestamp.ts new file mode 100644 index 000000000..bdd8a50dc --- /dev/null +++ b/apps/drec-api/src/validations/timestamp.ts @@ -0,0 +1,42 @@ +import { + registerDecorator, + ValidationArguments, + ValidationOptions, + ValidatorConstraint, + ValidatorConstraintInterface, +} from 'class-validator'; +import moment from 'moment'; + +const supportedFormats = [ + 'YYYY-MM-DD HH:mm:ss', + 'YYYY-MM-DD HH:mm:ss.SSS', + 'YYYY-MM-DD HH:mm:ss.SS', + 'YYYY-MM-DD HH:mm:ss.S', +]; + +@ValidatorConstraint({ async: false }) +export class IsValidTimestampConstraint + implements ValidatorConstraintInterface +{ + validate(value: string): boolean { + return supportedFormats.some((format) => + moment(value, format, true).isValid(), + ); + } + + defaultMessage(args: ValidationArguments): string { + return `Invalid date format for ${args.property}. Expected format: YYYY-MM-DD hh:mm:ss (with optional milliseconds). Please ensure it's valid in the timezone.`; + } +} + +export function IsTimestamp(validationOptions?: ValidationOptions) { + return function (object: Record, propertyName: string): void { + registerDecorator({ + target: object.constructor, + propertyName: propertyName, + options: validationOptions, + constraints: [], + validator: IsValidTimestampConstraint, + }); + }; +}