improve tests, handle coordinates precision
This commit is contained in:
parent
4ad00b96c0
commit
211bee2c70
|
@ -38,8 +38,8 @@ CREATE TABLE "waypoint" (
|
|||
"uuid" UUID NOT NULL,
|
||||
"adUuid" UUID NOT NULL,
|
||||
"position" SMALLINT NOT NULL,
|
||||
"lon" DOUBLE PRECISION NOT NULL,
|
||||
"lat" DOUBLE PRECISION NOT NULL,
|
||||
"lon" DECIMAL(9,6) NOT NULL,
|
||||
"lat" DECIMAL(8,6) NOT NULL,
|
||||
"name" TEXT,
|
||||
"houseNumber" TEXT,
|
||||
"street" TEXT,
|
|
@ -47,8 +47,8 @@ model Waypoint {
|
|||
uuid String @id @default(uuid()) @db.Uuid
|
||||
adUuid String @db.Uuid
|
||||
position Int @db.SmallInt
|
||||
lon Float
|
||||
lat Float
|
||||
lon Decimal @db.Decimal(9, 6)
|
||||
lat Decimal @db.Decimal(8, 6)
|
||||
name String?
|
||||
houseNumber String?
|
||||
street String?
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { Paginated } from '../ddd';
|
||||
|
||||
export abstract class PaginatedResponseDto<T> extends Paginated<T> {
|
||||
readonly count: number;
|
||||
readonly limit: number;
|
||||
readonly total: number;
|
||||
readonly perPage: number;
|
||||
readonly page: number;
|
||||
abstract readonly data: readonly T[];
|
||||
}
|
||||
|
|
|
@ -2,13 +2,16 @@ import { EventEmitter2 } from '@nestjs/event-emitter';
|
|||
import { AggregateRoot, Mapper, RepositoryPort } from '../ddd';
|
||||
import { ObjectLiteral } from '../types';
|
||||
import { LoggerPort } from '../ports/logger.port';
|
||||
import { None, Option, Some } from 'oxide.ts';
|
||||
import {
|
||||
PrismaRawRepositoryPort,
|
||||
PrismaRepositoryPort,
|
||||
} from '../ports/prisma-repository.port';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { ConflictException, DatabaseErrorException } from '@libs/exceptions';
|
||||
import {
|
||||
ConflictException,
|
||||
DatabaseErrorException,
|
||||
NotFoundException,
|
||||
} from '@libs/exceptions';
|
||||
|
||||
export abstract class PrismaRepositoryBase<
|
||||
Aggregate extends AggregateRoot<any>,
|
||||
|
@ -24,12 +27,13 @@ export abstract class PrismaRepositoryBase<
|
|||
protected readonly logger: LoggerPort,
|
||||
) {}
|
||||
|
||||
async findOneById(id: string, include?: any): Promise<Option<Aggregate>> {
|
||||
async findOneById(id: string, include?: any): Promise<Aggregate> {
|
||||
const entity = await this.prisma.findUnique({
|
||||
where: { uuid: id },
|
||||
include,
|
||||
});
|
||||
return entity ? Some(this.mapper.toDomain(entity)) : None;
|
||||
if (entity) return this.mapper.toDomain(entity);
|
||||
throw new NotFoundException('Record not found');
|
||||
}
|
||||
|
||||
async insert(entity: Aggregate): Promise<void> {
|
||||
|
@ -52,7 +56,6 @@ export abstract class PrismaRepositoryBase<
|
|||
await this.prisma.$queryRaw`SELECT 1`;
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseErrorException(e.message);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
import { OrderBy, PaginatedQueryParams } from './repository.port';
|
||||
|
||||
/**
|
||||
* Base class for regular queries
|
||||
*/
|
||||
export abstract class QueryBase {}
|
||||
|
||||
/**
|
||||
* Base class for paginated queries
|
||||
*/
|
||||
export abstract class PaginatedQueryBase extends QueryBase {
|
||||
perPage: number;
|
||||
offset: number;
|
||||
orderBy: OrderBy;
|
||||
page: number;
|
||||
|
||||
constructor(props: PaginatedParams<PaginatedQueryBase>) {
|
||||
super();
|
||||
this.perPage = props.perPage || 10;
|
||||
this.offset = props.page ? props.page * this.perPage : 0;
|
||||
this.page = props.page || 0;
|
||||
this.orderBy = props.orderBy || { field: true, param: 'desc' };
|
||||
}
|
||||
}
|
||||
|
||||
// Paginated query parameters
|
||||
export type PaginatedParams<T> = Omit<
|
||||
T,
|
||||
'perPage' | 'offset' | 'orderBy' | 'page'
|
||||
> &
|
||||
Partial<Omit<PaginatedQueryParams, 'offset'>>;
|
|
@ -1,5 +1,3 @@
|
|||
import { Option } from 'oxide.ts';
|
||||
|
||||
/* Most of repositories will probably need generic
|
||||
save/find/delete operations, so it's easier
|
||||
to have some shared interfaces.
|
||||
|
@ -8,14 +6,14 @@ import { Option } from 'oxide.ts';
|
|||
*/
|
||||
|
||||
export class Paginated<T> {
|
||||
readonly count: number;
|
||||
readonly limit: number;
|
||||
readonly total: number;
|
||||
readonly perPage: number;
|
||||
readonly page: number;
|
||||
readonly data: readonly T[];
|
||||
|
||||
constructor(props: Paginated<T>) {
|
||||
this.count = props.count;
|
||||
this.limit = props.limit;
|
||||
this.total = props.total;
|
||||
this.perPage = props.perPage;
|
||||
this.page = props.page;
|
||||
this.data = props.data;
|
||||
}
|
||||
|
@ -24,7 +22,7 @@ export class Paginated<T> {
|
|||
export type OrderBy = { field: string | true; param: 'asc' | 'desc' };
|
||||
|
||||
export type PaginatedQueryParams = {
|
||||
limit: number;
|
||||
perPage: number;
|
||||
page: number;
|
||||
offset: number;
|
||||
orderBy: OrderBy;
|
||||
|
@ -32,7 +30,7 @@ export type PaginatedQueryParams = {
|
|||
|
||||
export interface RepositoryPort<Entity> {
|
||||
insert(entity: Entity | Entity[]): Promise<void>;
|
||||
findOneById(id: string): Promise<Option<Entity>>;
|
||||
findOneById(id: string, include?: any): Promise<Entity>;
|
||||
healthCheck(): Promise<boolean>;
|
||||
// findAll(): Promise<Entity[]>;
|
||||
// findAllPaginated(params: PaginatedQueryParams): Promise<Paginated<Entity>>;
|
||||
|
|
|
@ -62,49 +62,49 @@ export class AdMapper
|
|||
fromDate: new Date(copy.fromDate),
|
||||
toDate: new Date(copy.toDate),
|
||||
monTime: copy.schedule.mon
|
||||
? this.timeConverter.dateTimeToUtc(
|
||||
? this.timeConverter.localDateTimeToUtc(
|
||||
copy.fromDate,
|
||||
copy.schedule.mon,
|
||||
timezone,
|
||||
)
|
||||
: undefined,
|
||||
tueTime: copy.schedule.tue
|
||||
? this.timeConverter.dateTimeToUtc(
|
||||
? this.timeConverter.localDateTimeToUtc(
|
||||
copy.fromDate,
|
||||
copy.schedule.tue,
|
||||
timezone,
|
||||
)
|
||||
: undefined,
|
||||
wedTime: copy.schedule.wed
|
||||
? this.timeConverter.dateTimeToUtc(
|
||||
? this.timeConverter.localDateTimeToUtc(
|
||||
copy.fromDate,
|
||||
copy.schedule.wed,
|
||||
timezone,
|
||||
)
|
||||
: undefined,
|
||||
thuTime: copy.schedule.thu
|
||||
? this.timeConverter.dateTimeToUtc(
|
||||
? this.timeConverter.localDateTimeToUtc(
|
||||
copy.fromDate,
|
||||
copy.schedule.thu,
|
||||
timezone,
|
||||
)
|
||||
: undefined,
|
||||
friTime: copy.schedule.fri
|
||||
? this.timeConverter.dateTimeToUtc(
|
||||
? this.timeConverter.localDateTimeToUtc(
|
||||
copy.fromDate,
|
||||
copy.schedule.fri,
|
||||
timezone,
|
||||
)
|
||||
: undefined,
|
||||
satTime: copy.schedule.sat
|
||||
? this.timeConverter.dateTimeToUtc(
|
||||
? this.timeConverter.localDateTimeToUtc(
|
||||
copy.fromDate,
|
||||
copy.schedule.sat,
|
||||
timezone,
|
||||
)
|
||||
: undefined,
|
||||
sunTime: copy.schedule.sun
|
||||
? this.timeConverter.dateTimeToUtc(
|
||||
? this.timeConverter.localDateTimeToUtc(
|
||||
copy.fromDate,
|
||||
copy.schedule.sun,
|
||||
timezone,
|
||||
|
@ -143,6 +143,11 @@ export class AdMapper
|
|||
};
|
||||
|
||||
toDomain = (record: AdReadModel): AdEntity => {
|
||||
const timezone = this.timezoneFinder.timezones(
|
||||
record.waypoints[0].lon,
|
||||
record.waypoints[0].lat,
|
||||
this.defaultParams.DEFAULT_TIMEZONE,
|
||||
)[0];
|
||||
const entity = new AdEntity({
|
||||
id: record.uuid,
|
||||
createdAt: new Date(record.createdAt),
|
||||
|
@ -152,13 +157,23 @@ export class AdMapper
|
|||
driver: record.driver,
|
||||
passenger: record.passenger,
|
||||
frequency: Frequency[record.frequency],
|
||||
fromDate: record.fromDate.toISOString(),
|
||||
toDate: record.toDate.toISOString(),
|
||||
fromDate: record.fromDate.toISOString().split('T')[0],
|
||||
toDate: record.toDate.toISOString().split('T')[0],
|
||||
schedule: {
|
||||
mon: record.monTime?.toISOString(),
|
||||
tue: record.tueTime?.toISOString(),
|
||||
wed: record.wedTime?.toISOString(),
|
||||
thu: record.thuTime?.toISOString(),
|
||||
wed: record.wedTime
|
||||
? this.timeConverter.utcDatetimeToLocalTime(
|
||||
record.wedTime.toISOString(),
|
||||
timezone,
|
||||
)
|
||||
: undefined,
|
||||
thu: record.thuTime
|
||||
? this.timeConverter.utcDatetimeToLocalTime(
|
||||
record.thuTime.toISOString(),
|
||||
timezone,
|
||||
)
|
||||
: undefined,
|
||||
fri: record.friTime?.toISOString(),
|
||||
sat: record.satTime?.toISOString(),
|
||||
sun: record.sunTime?.toISOString(),
|
||||
|
@ -198,7 +213,27 @@ export class AdMapper
|
|||
toResponse = (entity: AdEntity): AdResponseDto => {
|
||||
const props = entity.getProps();
|
||||
const response = new AdResponseDto(entity);
|
||||
response.uuid = props.id;
|
||||
response.userId = props.userId;
|
||||
response.driver = props.driver;
|
||||
response.passenger = props.passenger;
|
||||
response.frequency = props.frequency;
|
||||
response.fromDate = props.fromDate;
|
||||
response.toDate = props.toDate;
|
||||
response.schedule = { ...props.schedule };
|
||||
response.marginDurations = { ...props.marginDurations };
|
||||
response.seatsProposed = props.seatsProposed;
|
||||
response.seatsRequested = props.seatsRequested;
|
||||
response.waypoints = props.waypoints.map((waypoint: WaypointProps) => ({
|
||||
position: waypoint.position,
|
||||
name: waypoint.address.name,
|
||||
houseNumber: waypoint.address.houseNumber,
|
||||
street: waypoint.address.street,
|
||||
postalCode: waypoint.address.postalCode,
|
||||
locality: waypoint.address.locality,
|
||||
country: waypoint.address.country,
|
||||
lon: waypoint.address.coordinates.lon,
|
||||
lat: waypoint.address.coordinates.lat,
|
||||
}));
|
||||
return response;
|
||||
};
|
||||
|
||||
|
|
|
@ -20,12 +20,15 @@ import { CreateAdService } from './core/commands/create-ad/create-ad.service';
|
|||
import { TimezoneFinder } from './infrastructure/timezone-finder';
|
||||
import { PrismaService } from '@libs/db/prisma.service';
|
||||
import { TimeConverter } from './infrastructure/time-converter';
|
||||
import { FindAdByIdGrpcController } from './interface/grpc-controllers/find-ad-by-id.grpc.controller';
|
||||
import { FindAdByIdQueryHandler } from './core/queries/find-ad-by-id/find-ad-by-id.query-handler';
|
||||
|
||||
@Module({
|
||||
imports: [CqrsModule],
|
||||
controllers: [CreateAdGrpcController],
|
||||
controllers: [CreateAdGrpcController, FindAdByIdGrpcController],
|
||||
providers: [
|
||||
CreateAdService,
|
||||
FindAdByIdQueryHandler,
|
||||
PrismaService,
|
||||
AdMapper,
|
||||
{
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
export interface TimeConverterPort {
|
||||
dateTimeToUtc(
|
||||
localDateTimeToUtc(
|
||||
date: string,
|
||||
time: string,
|
||||
timezone: string,
|
||||
dst?: boolean,
|
||||
): Date;
|
||||
utcDatetimeToLocalTime(isoString: string, timezone: string): string;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
import { IQueryHandler, QueryHandler } from '@nestjs/cqrs';
|
||||
import { FindAdByIdQuery } from './find-ad-by-id.query';
|
||||
import { AD_REPOSITORY } from '@modules/ad/ad.di-tokens';
|
||||
import { AdRepositoryPort } from '../../ports/ad.repository.port';
|
||||
import { Inject } from '@nestjs/common';
|
||||
import { AdEntity } from '../../ad.entity';
|
||||
|
||||
@QueryHandler(FindAdByIdQuery)
|
||||
export class FindAdByIdQueryHandler implements IQueryHandler {
|
||||
constructor(
|
||||
@Inject(AD_REPOSITORY)
|
||||
private readonly repository: AdRepositoryPort,
|
||||
) {}
|
||||
async execute(query: FindAdByIdQuery): Promise<AdEntity> {
|
||||
return await this.repository.findOneById(query.id, { waypoints: true });
|
||||
}
|
||||
}
|
|
@ -1,9 +1,10 @@
|
|||
import { FindAdByIdRequestDTO } from '../../../interface/queries/find-ad-by-id/dtos/find-ad-by-id.request.dto';
|
||||
import { QueryBase } from '@libs/ddd/query.base';
|
||||
|
||||
export class FindAdByIdQuery {
|
||||
export class FindAdByIdQuery extends QueryBase {
|
||||
readonly id: string;
|
||||
|
||||
constructor(findAdByIdRequestDTO: FindAdByIdRequestDTO) {
|
||||
this.id = findAdByIdRequestDTO.id;
|
||||
constructor(id: string) {
|
||||
super();
|
||||
this.id = id;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import { DateTime, TimeZone } from 'timezonecomplete';
|
|||
|
||||
@Injectable()
|
||||
export class TimeConverter implements TimeConverterPort {
|
||||
dateTimeToUtc = (
|
||||
localDateTimeToUtc = (
|
||||
date: string,
|
||||
time: string,
|
||||
timezone: string,
|
||||
|
@ -21,4 +21,16 @@ export class TimeConverter implements TimeConverterPort {
|
|||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
utcDatetimeToLocalTime = (isoString: string, timezone: string): string => {
|
||||
try {
|
||||
return new DateTime(isoString)
|
||||
.convert(TimeZone.zone(timezone))
|
||||
.toString()
|
||||
.split('T')[1]
|
||||
.substring(0, 5);
|
||||
} catch (e) {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
import { AutoMap } from '@automapper/classes';
|
||||
|
||||
export class AdPresenter {
|
||||
@AutoMap()
|
||||
id: string;
|
||||
}
|
|
@ -1,5 +1,43 @@
|
|||
import { ResponseBase } from '@libs/api/response.base';
|
||||
import { Frequency } from '@modules/ad/core/ad.types';
|
||||
|
||||
export class AdResponseDto extends ResponseBase {
|
||||
uuid: string;
|
||||
userId: string;
|
||||
driver: boolean;
|
||||
passenger: boolean;
|
||||
frequency: Frequency;
|
||||
fromDate: string;
|
||||
toDate: string;
|
||||
schedule: {
|
||||
mon?: string;
|
||||
tue?: string;
|
||||
wed?: string;
|
||||
thu?: string;
|
||||
fri?: string;
|
||||
sat?: string;
|
||||
sun?: string;
|
||||
};
|
||||
marginDurations: {
|
||||
mon?: number;
|
||||
tue?: number;
|
||||
wed?: number;
|
||||
thu?: number;
|
||||
fri?: number;
|
||||
sat?: number;
|
||||
sun?: number;
|
||||
};
|
||||
seatsProposed: number;
|
||||
seatsRequested: number;
|
||||
strict: boolean;
|
||||
waypoints: {
|
||||
position: number;
|
||||
name?: string;
|
||||
houseNumber?: string;
|
||||
street?: string;
|
||||
postalCode?: string;
|
||||
locality?: string;
|
||||
country: string;
|
||||
lon: number;
|
||||
lat: number;
|
||||
}[];
|
||||
}
|
||||
|
|
|
@ -2,8 +2,7 @@ import { Controller, UsePipes } from '@nestjs/common';
|
|||
import { CommandBus } from '@nestjs/cqrs';
|
||||
import { GrpcMethod, RpcException } from '@nestjs/microservices';
|
||||
import { RpcValidationPipe } from '../../../../utils/pipes/rpc.validation-pipe';
|
||||
import { AdPresenter } from '../ad.presenter';
|
||||
import { CreateAdRequestDTO } from './dtos/create-ad.request.dto';
|
||||
import { CreateAdRequestDto } from './dtos/create-ad.request.dto';
|
||||
import { CreateAdCommand } from '../../core/commands/create-ad/create-ad.command';
|
||||
import { Result, match } from 'oxide.ts';
|
||||
import { AggregateID } from '@libs/ddd';
|
||||
|
@ -21,7 +20,7 @@ export class CreateAdGrpcController {
|
|||
constructor(private readonly commandBus: CommandBus) {}
|
||||
|
||||
@GrpcMethod('AdsService', 'Create')
|
||||
async create(data: CreateAdRequestDTO): Promise<AdPresenter> {
|
||||
async create(data: CreateAdRequestDto): Promise<IdResponse> {
|
||||
const result: Result<AggregateID, AdAlreadyExistsError> =
|
||||
await this.commandBus.execute(new CreateAdCommand(data));
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { AutoMap } from '@automapper/classes';
|
||||
import { IsOptional, IsString } from 'class-validator';
|
||||
import { CoordinatesDTO } from './coordinates.dto';
|
||||
import { CoordinatesDto as CoordinatesDto } from './coordinates.dto';
|
||||
|
||||
export class AddressDTO extends CoordinatesDTO {
|
||||
export class AddressDto extends CoordinatesDto {
|
||||
@IsOptional()
|
||||
@AutoMap()
|
||||
name?: string;
|
||||
|
|
|
@ -1,11 +1,19 @@
|
|||
import { AutoMap } from '@automapper/classes';
|
||||
import { Transform } from 'class-transformer';
|
||||
import { IsLatitude, IsLongitude } from 'class-validator';
|
||||
import { toPrecision } from './validators/to-precision';
|
||||
|
||||
export class CoordinatesDTO {
|
||||
export class CoordinatesDto {
|
||||
@Transform(({ value }) => toPrecision(value, 6), {
|
||||
toClassOnly: true,
|
||||
})
|
||||
@IsLongitude()
|
||||
@AutoMap()
|
||||
lon: number;
|
||||
|
||||
@Transform(({ value }) => toPrecision(value, 6), {
|
||||
toClassOnly: true,
|
||||
})
|
||||
@IsLatitude()
|
||||
@AutoMap()
|
||||
lat: number;
|
||||
|
|
|
@ -11,15 +11,15 @@ import {
|
|||
IsISO8601,
|
||||
} from 'class-validator';
|
||||
import { Transform, Type } from 'class-transformer';
|
||||
import { ScheduleDTO } from './schedule.dto';
|
||||
import { MarginDurationsDTO } from './margin-durations.dto';
|
||||
import { WaypointDTO } from './waypoint.dto';
|
||||
import { ScheduleDto } from './schedule.dto';
|
||||
import { MarginDurationsDto } from './margin-durations.dto';
|
||||
import { WaypointDto } from './waypoint.dto';
|
||||
import { intToFrequency } from './validators/frequency.mapping';
|
||||
import { IsSchedule } from './validators/decorators/is-schedule.validator';
|
||||
import { HasValidPositionIndexes } from './validators/decorators/valid-position-indexes.validator';
|
||||
import { Frequency } from '@modules/ad/core/ad.types';
|
||||
|
||||
export class CreateAdRequestDTO {
|
||||
export class CreateAdRequestDto {
|
||||
@IsUUID(4)
|
||||
@AutoMap()
|
||||
userId: string;
|
||||
|
@ -55,17 +55,17 @@ export class CreateAdRequestDTO {
|
|||
@AutoMap()
|
||||
toDate: string;
|
||||
|
||||
@Type(() => ScheduleDTO)
|
||||
@Type(() => ScheduleDto)
|
||||
@IsSchedule()
|
||||
@ValidateNested({ each: true })
|
||||
@AutoMap()
|
||||
schedule: ScheduleDTO;
|
||||
schedule: ScheduleDto;
|
||||
|
||||
@IsOptional()
|
||||
@Type(() => MarginDurationsDTO)
|
||||
@Type(() => MarginDurationsDto)
|
||||
@ValidateNested({ each: true })
|
||||
@AutoMap()
|
||||
marginDurations?: MarginDurationsDTO;
|
||||
marginDurations?: MarginDurationsDto;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
|
@ -82,10 +82,11 @@ export class CreateAdRequestDTO {
|
|||
@AutoMap()
|
||||
strict?: boolean;
|
||||
|
||||
@Type(() => WaypointDto)
|
||||
@IsArray()
|
||||
@ArrayMinSize(2)
|
||||
@HasValidPositionIndexes()
|
||||
@ValidateNested({ each: true })
|
||||
@AutoMap()
|
||||
waypoints: WaypointDTO[];
|
||||
waypoints: WaypointDto[];
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { IsNotEmpty, IsString } from 'class-validator';
|
||||
|
||||
export class FindAdByIdRequestDTO {
|
||||
export class FindAdByIdRequestDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
id: string;
|
|
@ -1,7 +1,7 @@
|
|||
import { AutoMap } from '@automapper/classes';
|
||||
import { IsInt, IsOptional } from 'class-validator';
|
||||
|
||||
export class MarginDurationsDTO {
|
||||
export class MarginDurationsDto {
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@AutoMap()
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { AutoMap } from '@automapper/classes';
|
||||
import { IsOptional, IsMilitaryTime } from 'class-validator';
|
||||
|
||||
export class ScheduleDTO {
|
||||
export class ScheduleDto {
|
||||
@IsOptional()
|
||||
@IsMilitaryTime()
|
||||
@AutoMap()
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { ValidateBy, ValidationOptions, buildMessage } from 'class-validator';
|
||||
import { hasValidPositionIndexes } from '../waypoint-position';
|
||||
import { WaypointDTO } from '../../waypoint.dto';
|
||||
import { WaypointDto } from '../../waypoint.dto';
|
||||
|
||||
export const HasValidPositionIndexes = (
|
||||
validationOptions?: ValidationOptions,
|
||||
|
@ -10,7 +10,7 @@ export const HasValidPositionIndexes = (
|
|||
name: '',
|
||||
constraints: [],
|
||||
validator: {
|
||||
validate: (waypoints: WaypointDTO[]): boolean =>
|
||||
validate: (waypoints: WaypointDto[]): boolean =>
|
||||
hasValidPositionIndexes(waypoints),
|
||||
defaultMessage: buildMessage(
|
||||
() => `invalid waypoints positions`,
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
export const toPrecision = (input: number, precision: number): number => {
|
||||
const multiplier = 10 ** precision;
|
||||
return Math.round((input + Number.EPSILON) * multiplier) / multiplier;
|
||||
};
|
|
@ -1,6 +1,6 @@
|
|||
import { WaypointDTO } from '../waypoint.dto';
|
||||
import { WaypointDto } from '../waypoint.dto';
|
||||
|
||||
export const hasValidPositionIndexes = (waypoints: WaypointDTO[]): boolean => {
|
||||
export const hasValidPositionIndexes = (waypoints: WaypointDto[]): boolean => {
|
||||
if (!waypoints) return;
|
||||
if (waypoints.every((waypoint) => waypoint.position === undefined))
|
||||
return true;
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { AutoMap } from '@automapper/classes';
|
||||
import { IsInt, IsOptional } from 'class-validator';
|
||||
import { AddressDTO } from './address.dto';
|
||||
import { AddressDto } from './address.dto';
|
||||
|
||||
export class WaypointDTO extends AddressDTO {
|
||||
export class WaypointDto extends AddressDto {
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@AutoMap()
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
import { Controller, UsePipes } from '@nestjs/common';
|
||||
import { QueryBus } from '@nestjs/cqrs';
|
||||
import { GrpcMethod, RpcException } from '@nestjs/microservices';
|
||||
import { RpcValidationPipe } from '@utils/pipes/rpc.validation-pipe';
|
||||
import { FindAdByIdRequestDto } from './dtos/find-ad-by-id.request.dto';
|
||||
import { FindAdByIdQuery } from '@modules/ad/core/queries/find-ad-by-id/find-ad-by-id.query';
|
||||
import { AdResponseDto } from '../dtos/ad.response.dto';
|
||||
import { AdEntity } from '@modules/ad/core/ad.entity';
|
||||
import { AdMapper } from '@modules/ad/ad.mapper';
|
||||
|
||||
@UsePipes(
|
||||
new RpcValidationPipe({
|
||||
whitelist: false,
|
||||
forbidUnknownValues: false,
|
||||
}),
|
||||
)
|
||||
@Controller()
|
||||
export class FindAdByIdGrpcController {
|
||||
constructor(
|
||||
protected readonly mapper: AdMapper,
|
||||
private readonly queryBus: QueryBus,
|
||||
) {}
|
||||
|
||||
@GrpcMethod('AdsService', 'FindOneById')
|
||||
async findOnebyId(data: FindAdByIdRequestDto): Promise<AdResponseDto> {
|
||||
try {
|
||||
const ad: AdEntity = await this.queryBus.execute(
|
||||
new FindAdByIdQuery(data.id),
|
||||
);
|
||||
return this.mapper.toResponse(ad);
|
||||
} catch (e) {
|
||||
throw new RpcException({
|
||||
code: e.code,
|
||||
message: e.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
import { Mapper } from '@automapper/core';
|
||||
import { InjectMapper } from '@automapper/nestjs';
|
||||
import { Controller, UsePipes } from '@nestjs/common';
|
||||
import { QueryBus } from '@nestjs/cqrs';
|
||||
import { GrpcMethod, RpcException } from '@nestjs/microservices';
|
||||
import { RpcValidationPipe } from '../../../../../utils/pipes/rpc.validation-pipe';
|
||||
import { FindAdByIdRequestDTO } from './dtos/find-ad-by-id.request.dto';
|
||||
import { AdPresenter } from '../../ad.presenter';
|
||||
import { FindAdByIdQuery } from '../../../core/queries/find-ad-by-id/find-ad-by-id.query';
|
||||
import { AdEntity } from '../../../core/ad.entity';
|
||||
|
||||
@UsePipes(
|
||||
new RpcValidationPipe({
|
||||
whitelist: false,
|
||||
forbidUnknownValues: false,
|
||||
}),
|
||||
)
|
||||
@Controller()
|
||||
export class FindAdByIdGrpcController {
|
||||
constructor(
|
||||
private readonly queryBus: QueryBus,
|
||||
@InjectMapper() private readonly mapper: Mapper,
|
||||
) {}
|
||||
|
||||
@GrpcMethod('AdsService', 'FindOneById')
|
||||
async findOnebyId(data: FindAdByIdRequestDTO): Promise<AdPresenter> {
|
||||
try {
|
||||
const ad = await this.queryBus.execute(new FindAdByIdQuery(data));
|
||||
return this.mapper.map(ad, AdEntity, AdPresenter);
|
||||
} catch (e) {
|
||||
throw new RpcException({
|
||||
code: e.code,
|
||||
message: e.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,10 +3,12 @@ import {
|
|||
AD_REPOSITORY,
|
||||
PARAMS_PROVIDER,
|
||||
TIMEZONE_FINDER,
|
||||
TIME_CONVERTER,
|
||||
} from '@modules/ad/ad.di-tokens';
|
||||
import { AdMapper } from '@modules/ad/ad.mapper';
|
||||
import { AdRepository } from '@modules/ad/infrastructure/ad.repository';
|
||||
import { DefaultParamsProvider } from '@modules/ad/infrastructure/default-params-provider';
|
||||
import { TimeConverter } from '@modules/ad/infrastructure/time-converter';
|
||||
import { TimezoneFinder } from '@modules/ad/infrastructure/timezone-finder';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { EventEmitterModule } from '@nestjs/event-emitter';
|
||||
|
@ -274,6 +276,10 @@ describe('Ad Repository', () => {
|
|||
provide: TIMEZONE_FINDER,
|
||||
useClass: TimezoneFinder,
|
||||
},
|
||||
{
|
||||
provide: TIME_CONVERTER,
|
||||
useClass: TimeConverter,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
prismaService = module.get<PrismaService>(PrismaService);
|
||||
|
@ -416,7 +422,7 @@ describe('Ad Repository', () => {
|
|||
waypoints: true,
|
||||
});
|
||||
|
||||
expect(result.unwrap().id).toBe(baseUuid.uuid);
|
||||
expect(result.id).toBe(baseUuid.uuid);
|
||||
});
|
||||
|
||||
// it('should return null', async () => {
|
||||
|
|
|
@ -45,8 +45,8 @@ const adEntity: AdEntity = new AdEntity({
|
|||
postalCode: '54000',
|
||||
country: 'France',
|
||||
coordinates: {
|
||||
lat: 48.68944505415954,
|
||||
lon: 6.176510296462267,
|
||||
lat: 48.689445,
|
||||
lon: 6.1765102,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -103,8 +103,8 @@ const adReadModel: AdReadModel = {
|
|||
locality: 'Nancy',
|
||||
postalCode: '54000',
|
||||
country: 'France',
|
||||
lat: 48.68944505415954,
|
||||
lon: 6.176510296462267,
|
||||
lat: 48.689445,
|
||||
lon: 6.1765102,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
},
|
||||
|
@ -163,12 +163,13 @@ const mockTimezoneFinder: TimezoneFinderPort = {
|
|||
};
|
||||
|
||||
const mockTimeConverter: TimeConverterPort = {
|
||||
dateTimeToUtc: jest
|
||||
localDateTimeToUtc: jest
|
||||
.fn()
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
.mockImplementation((datetime: Date, timezone: string, dst?: boolean) => {
|
||||
return datetime;
|
||||
}),
|
||||
utcDatetimeToLocalTime: jest.fn(),
|
||||
};
|
||||
|
||||
describe('Ad Mapper', () => {
|
||||
|
@ -208,13 +209,13 @@ describe('Ad Mapper', () => {
|
|||
it('should map persisted data to domain entity', async () => {
|
||||
const mapped: AdEntity = adMapper.toDomain(adReadModel);
|
||||
expect(mapped.getProps().waypoints[0].address.coordinates.lat).toBe(
|
||||
48.68944505415954,
|
||||
48.689445,
|
||||
);
|
||||
expect(mapped.getProps().waypoints[1].address.coordinates.lon).toBe(2.3522);
|
||||
});
|
||||
|
||||
it('should map domain entity to response', async () => {
|
||||
const mapped: AdResponseDto = adMapper.toResponse(adEntity);
|
||||
expect(mapped.uuid).toBe('c160cf8c-f057-4962-841f-3ad68346df44');
|
||||
expect(mapped.id).toBe('c160cf8c-f057-4962-841f-3ad68346df44');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -2,8 +2,8 @@ import { Test, TestingModule } from '@nestjs/testing';
|
|||
import { CreateAdService } from '@modules/ad/core/commands/create-ad/create-ad.service';
|
||||
import { AD_REPOSITORY, PARAMS_PROVIDER } from '@modules/ad/ad.di-tokens';
|
||||
import { DefaultParamsProviderPort } from '@modules/ad/core/ports/default-params-provider.port';
|
||||
import { WaypointDTO } from '@modules/ad/interface/grpc-controllers/dtos/waypoint.dto';
|
||||
import { CreateAdRequestDTO } from '@modules/ad/interface/grpc-controllers/dtos/create-ad.request.dto';
|
||||
import { WaypointDto } from '@modules/ad/interface/grpc-controllers/dtos/waypoint.dto';
|
||||
import { CreateAdRequestDto } from '@modules/ad/interface/grpc-controllers/dtos/create-ad.request.dto';
|
||||
import { Frequency } from '@modules/ad/core/ad.types';
|
||||
import { CreateAdCommand } from '@modules/ad/core/commands/create-ad/create-ad.command';
|
||||
import { Result } from 'oxide.ts';
|
||||
|
@ -12,7 +12,7 @@ import { AdAlreadyExistsError } from '@modules/ad/core/ad.errors';
|
|||
import { AdEntity } from '@modules/ad/core/ad.entity';
|
||||
import { ConflictException } from '@libs/exceptions';
|
||||
|
||||
const originWaypoint: WaypointDTO = {
|
||||
const originWaypoint: WaypointDto = {
|
||||
position: 0,
|
||||
lon: 48.68944505415954,
|
||||
lat: 6.176510296462267,
|
||||
|
@ -22,7 +22,7 @@ const originWaypoint: WaypointDTO = {
|
|||
postalCode: '54000',
|
||||
country: 'France',
|
||||
};
|
||||
const destinationWaypoint: WaypointDTO = {
|
||||
const destinationWaypoint: WaypointDto = {
|
||||
position: 1,
|
||||
lon: 48.8566,
|
||||
lat: 2.3522,
|
||||
|
@ -30,7 +30,7 @@ const destinationWaypoint: WaypointDTO = {
|
|||
postalCode: '75000',
|
||||
country: 'France',
|
||||
};
|
||||
const punctualCreateAdRequest: CreateAdRequestDTO = {
|
||||
const punctualCreateAdRequest: CreateAdRequestDto = {
|
||||
userId: '4eb6a6af-ecfd-41c3-9118-473a507014d4',
|
||||
fromDate: '2023-12-21',
|
||||
toDate: '2023-12-21',
|
||||
|
|
|
@ -5,48 +5,81 @@ describe('Time Converter', () => {
|
|||
const timeConverter: TimeConverter = new TimeConverter();
|
||||
expect(timeConverter).toBeDefined();
|
||||
});
|
||||
it('should convert a paris datetime to utc', () => {
|
||||
const timeConverter: TimeConverter = new TimeConverter();
|
||||
const parisDate = '2023-06-22';
|
||||
const parisTime = '08:00';
|
||||
const utcDatetime = timeConverter.dateTimeToUtc(
|
||||
parisDate,
|
||||
parisTime,
|
||||
'Europe/Paris',
|
||||
);
|
||||
expect(utcDatetime.toISOString()).toEqual('2023-06-22T06:00:00.000Z');
|
||||
|
||||
describe('localDateTimeToUtc', () => {
|
||||
it('should convert a paris datetime to utc', () => {
|
||||
const timeConverter: TimeConverter = new TimeConverter();
|
||||
const parisDate = '2023-06-22';
|
||||
const parisTime = '08:00';
|
||||
const utcDatetime = timeConverter.localDateTimeToUtc(
|
||||
parisDate,
|
||||
parisTime,
|
||||
'Europe/Paris',
|
||||
);
|
||||
expect(utcDatetime.toISOString()).toEqual('2023-06-22T06:00:00.000Z');
|
||||
});
|
||||
it('should return undefined if date is invalid', () => {
|
||||
const timeConverter: TimeConverter = new TimeConverter();
|
||||
const parisDate = '2023-16-22';
|
||||
const parisTime = '08:00';
|
||||
const utcDatetime = timeConverter.localDateTimeToUtc(
|
||||
parisDate,
|
||||
parisTime,
|
||||
'Europe/Paris',
|
||||
);
|
||||
expect(utcDatetime).toBeUndefined();
|
||||
});
|
||||
it('should return undefined if time is invalid', () => {
|
||||
const timeConverter: TimeConverter = new TimeConverter();
|
||||
const parisDate = '2023-06-22';
|
||||
const parisTime = '28:00';
|
||||
const utcDatetime = timeConverter.localDateTimeToUtc(
|
||||
parisDate,
|
||||
parisTime,
|
||||
'Europe/Paris',
|
||||
);
|
||||
expect(utcDatetime).toBeUndefined();
|
||||
});
|
||||
it('should return undefined if timezone is invalid', () => {
|
||||
const timeConverter: TimeConverter = new TimeConverter();
|
||||
const parisDate = '2023-06-22';
|
||||
const parisTime = '08:00';
|
||||
const utcDatetime = timeConverter.localDateTimeToUtc(
|
||||
parisDate,
|
||||
parisTime,
|
||||
'Foo/Bar',
|
||||
);
|
||||
expect(utcDatetime).toBeUndefined();
|
||||
});
|
||||
});
|
||||
it('should return undefined if date is invalid', () => {
|
||||
const timeConverter: TimeConverter = new TimeConverter();
|
||||
const parisDate = '2023-16-22';
|
||||
const parisTime = '08:00';
|
||||
const utcDatetime = timeConverter.dateTimeToUtc(
|
||||
parisDate,
|
||||
parisTime,
|
||||
'Europe/Paris',
|
||||
);
|
||||
expect(utcDatetime).toBeUndefined();
|
||||
});
|
||||
it('should return undefined if time is invalid', () => {
|
||||
const timeConverter: TimeConverter = new TimeConverter();
|
||||
const parisDate = '2023-06-22';
|
||||
const parisTime = '28:00';
|
||||
const utcDatetime = timeConverter.dateTimeToUtc(
|
||||
parisDate,
|
||||
parisTime,
|
||||
'Europe/Paris',
|
||||
);
|
||||
expect(utcDatetime).toBeUndefined();
|
||||
});
|
||||
it('should return undefined if timezone is invalid', () => {
|
||||
const timeConverter: TimeConverter = new TimeConverter();
|
||||
const parisDate = '2023-06-22';
|
||||
const parisTime = '08:00';
|
||||
const utcDatetime = timeConverter.dateTimeToUtc(
|
||||
parisDate,
|
||||
parisTime,
|
||||
'Foo/Bar',
|
||||
);
|
||||
expect(utcDatetime).toBeUndefined();
|
||||
|
||||
describe('utcDatetimeToLocalTime', () => {
|
||||
it('should convert an utc datetime isostring to a paris local time', () => {
|
||||
const timeConverter: TimeConverter = new TimeConverter();
|
||||
const utcDatetimeIsostring = '2023-06-22T06:25:00.000Z';
|
||||
const parisTime = timeConverter.utcDatetimeToLocalTime(
|
||||
utcDatetimeIsostring,
|
||||
'Europe/Paris',
|
||||
);
|
||||
expect(parisTime).toBe('08:25');
|
||||
});
|
||||
it('should return undefined if isostring input is invalid', () => {
|
||||
const timeConverter: TimeConverter = new TimeConverter();
|
||||
const utcDatetimeIsostring = 'not_an_isostring';
|
||||
const parisTime = timeConverter.utcDatetimeToLocalTime(
|
||||
utcDatetimeIsostring,
|
||||
'Europe/Paris',
|
||||
);
|
||||
expect(parisTime).toBeUndefined();
|
||||
});
|
||||
it('should return undefined if timezone input is invalid', () => {
|
||||
const timeConverter: TimeConverter = new TimeConverter();
|
||||
const utcDatetimeIsostring = '2023-06-22T06:25:00.000Z';
|
||||
const parisTime = timeConverter.utcDatetimeToLocalTime(
|
||||
utcDatetimeIsostring,
|
||||
'Foo/Bar',
|
||||
);
|
||||
expect(parisTime).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { hasValidPositionIndexes } from '@modules/ad/interface/grpc-controllers/dtos/validators/waypoint-position';
|
||||
import { WaypointDTO } from '@modules/ad/interface/grpc-controllers/dtos/waypoint.dto';
|
||||
import { WaypointDto } from '@modules/ad/interface/grpc-controllers/dtos/waypoint.dto';
|
||||
|
||||
describe('addresses position validator', () => {
|
||||
const mockAddress1: WaypointDTO = {
|
||||
const mockAddress1: WaypointDto = {
|
||||
lon: 48.68944505415954,
|
||||
lat: 6.176510296462267,
|
||||
houseNumber: '5',
|
||||
|
@ -11,14 +11,14 @@ describe('addresses position validator', () => {
|
|||
postalCode: '54000',
|
||||
country: 'France',
|
||||
};
|
||||
const mockAddress2: WaypointDTO = {
|
||||
const mockAddress2: WaypointDto = {
|
||||
lon: 48.8566,
|
||||
lat: 2.3522,
|
||||
locality: 'Paris',
|
||||
postalCode: '75000',
|
||||
country: 'France',
|
||||
};
|
||||
const mockAddress3: WaypointDTO = {
|
||||
const mockAddress3: WaypointDto = {
|
||||
lon: 49.2628,
|
||||
lat: 4.0347,
|
||||
locality: 'Reims',
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { ArgumentMetadata } from '@nestjs/common';
|
||||
import { RpcValidationPipe } from '../../pipes/rpc.validation-pipe';
|
||||
import { FindAdByIdRequestDTO } from '../../../modules/ad/interface/queries/find-ad-by-id/dtos/find-ad-by-id.request.dto';
|
||||
import { FindAdByIdRequestDto } from '../../../modules/ad/interface/grpc-controllers/dtos/find-ad-by-id.request.dto';
|
||||
|
||||
describe('RpcValidationPipe', () => {
|
||||
it('should not validate request', async () => {
|
||||
|
@ -10,10 +10,10 @@ describe('RpcValidationPipe', () => {
|
|||
});
|
||||
const metadata: ArgumentMetadata = {
|
||||
type: 'body',
|
||||
metatype: FindAdByIdRequestDTO,
|
||||
metatype: FindAdByIdRequestDto,
|
||||
data: '',
|
||||
};
|
||||
await target.transform(<FindAdByIdRequestDTO>{}, metadata).catch((err) => {
|
||||
await target.transform(<FindAdByIdRequestDto>{}, metadata).catch((err) => {
|
||||
expect(err.message).toEqual('Rpc Exception');
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue