Skip to content

Commit

Permalink
feat(MS-19): save route api
Browse files Browse the repository at this point in the history
  • Loading branch information
High10Hunter committed Mar 29, 2024
1 parent 6953003 commit 8975aa7
Show file tree
Hide file tree
Showing 13 changed files with 397 additions and 2 deletions.
15 changes: 15 additions & 0 deletions src/modules/maps/domain/mappers/route/route.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export type TRouteRaw = {
id: string;
name: string;
path_display: object;
provider: string;
user_id: string;
};

export type TRouteOrm = {
id: string;
name: string;
pathDisplay: object;
provider: string;
userId: string;
};
33 changes: 33 additions & 0 deletions src/modules/maps/domain/mappers/route/route.mapper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Mapper } from '@core/domain/mapper';
import { Route } from '../../route';
import { UniqueEntityID } from '@core/domain/unique-entity-id';
import { TRouteOrm } from './route';

export class RouteMap implements Mapper<Route> {
public static async toDomain(raw: any): Promise<Route> {
const routeOrError = Route.create(
{
name: raw.name,
pathDisplay: raw.path_display,
provider: raw.provider,
userId: raw.user_id,
},
new UniqueEntityID(raw.id),
);

// eslint-disable-next-line no-console
routeOrError.isFailed ? console.log(routeOrError.getErrorValue()) : '';

return routeOrError.isSuccess ? routeOrError.getValue() : null;
}

public static async toPersistence(route: Route): Promise<TRouteOrm> {
return {
id: route.id.getStringValue(),
name: route.name,
pathDisplay: route.pathDisplay,
provider: route.provider,
userId: route.userId,
};
}
}
65 changes: 65 additions & 0 deletions src/modules/maps/domain/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { AggregateRootDomain } from '@core/domain/aggregate-root-domain';
import { UniqueEntityID } from '@core/domain/unique-entity-id';
import { RouteId } from './value-objects/route-id';
import { Result } from '@core/logic/errors-handler';
import { Guard } from '@core/logic/guard';

class PathDisplay {
overview_polyline?: string;
input_polyline?: string;
waypoint_order?: number[];
}

interface RouteProps {
name: string;
pathDisplay?: PathDisplay;
provider: string;
userId: string;
}

export class Route extends AggregateRootDomain<RouteProps> {
private constructor(props: RouteProps, id?: UniqueEntityID) {
super(props, id);
}

get id(): RouteId {
return RouteId.create(this._id).getValue();
}

get name(): string {
return this.props.name;
}

get pathDisplay(): PathDisplay {
return this.props.pathDisplay;
}

get provider(): string {
return this.props.provider;
}

get userId(): string {
return this.props.userId;
}

public static create(props: RouteProps, id?: UniqueEntityID): Result<Route> {
const guardResult = Guard.againstNullOrUndefinedBulk([
{ arg: props.name, argName: 'name' },
{ arg: props.provider, argName: 'provider' },
{ arg: props.userId, argName: 'userId' },
]);

if (guardResult.isFailed) {
return Result.fail<Route>(guardResult.getErrorValue());
}

const route = new Route(
{
...props,
},
id,
);

return Result.ok<Route>(route);
}
}
27 changes: 27 additions & 0 deletions src/modules/maps/domain/value-objects/route-id.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { UniqueEntityID } from '@core/domain/unique-entity-id';
import { ValueObject } from '@core/domain/value-object';
import { Result } from '@core/logic/errors-handler';
import { Guard } from '@core/logic/guard';

export class RouteId extends ValueObject<{ value: UniqueEntityID }> {
private constructor(value: UniqueEntityID) {
super({ value });
}

getStringValue(): string {
return this.props.value.toString();
}

getValue(): UniqueEntityID {
return this.props.value;
}

public static create(value: UniqueEntityID): Result<RouteId> {
const guardResult = Guard.againstNullOrUndefined(value, 'value');
if (guardResult.isFailed) {
return Result.fail<RouteId>(guardResult.getErrorValue());
}

return Result.ok<RouteId>(new RouteId(value));
}
}
45 changes: 43 additions & 2 deletions src/modules/maps/infra/restful-api/map.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,20 @@ import {
InternalServerErrorException,
Post,
} from '@nestjs/common';
import { QueryBus } from '@nestjs/cqrs';
import { CommandBus, QueryBus } from '@nestjs/cqrs';
import { ApiOperation, ApiResponse } from '@nestjs/swagger';
import { GetOptimizedRouteRequestDto } from '../../usecases/get-optimized-route/get-optimized-route.dto';
import { SaveRouteRequestDto } from '@modules/maps/usecases/save-route/save-route.dto';
import { SaveRouteResponse } from '@modules/maps/usecases/save-route/save-route.response';
import { SaveRouteCommand } from '@modules/maps/usecases/save-route/save-route.command';
import { DuplicateRouteName } from '@modules/maps/usecases/save-route/save-route.errors';

@Controller('maps')
export class MapController {
constructor(private readonly queryBus: QueryBus) {}
constructor(
private readonly queryBus: QueryBus,
private readonly commandBus: CommandBus,
) {}

@Post('geocode')
@HttpCode(HttpStatus.OK)
Expand Down Expand Up @@ -98,4 +105,38 @@ export class MapController {
}
}
}

@Post('save-route')
@HttpCode(HttpStatus.OK)
@ApiOperation({
tags: ['maps'],
operationId: 'save-route',
summary: 'Save Route',
description: 'Save optimized route',
})
@ApiResponse({
status: HttpStatus.OK,
description: 'Successful',
})
async saveRoute(
@Body() saveRouteRequestDto: SaveRouteRequestDto,
): Promise<SaveRouteResponse> {
const result = await this.commandBus.execute<
SaveRouteCommand,
SaveRouteResponse
>(new SaveRouteCommand(saveRouteRequestDto));

if (result.isSuccess()) {
return;
} else if (result.isFailure()) {
const error = result.value;

switch (error.constructor) {
case DuplicateRouteName:
throw new BadRequestException(error.getErrorValue());
default:
throw new InternalServerErrorException(error.getErrorValue());
}
}
}
}
8 changes: 8 additions & 0 deletions src/modules/maps/infra/restful-api/map.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { MapController } from './map.controller';
import { GoogleMapService } from '@modules/maps/services/providers/google-map.service';
import { GeocodeUseCase } from '@modules/maps/usecases/geocode/geocode.usecase';
import { GetOptimizedRouteUseCase } from '@modules/maps/usecases/get-optimized-route/get-optimized-route.usecase';
import { TypeOrmRouteRepository } from '@modules/maps/repos/implementations/typeorm.route.repository';
import { SaveRouteUseCase } from '@modules/maps/usecases/save-route/save-route.usecase';

@Module({
imports: [
Expand All @@ -27,10 +29,16 @@ import { GetOptimizedRouteUseCase } from '@modules/maps/usecases/get-optimized-r
providers: [
GeocodeUseCase,
GetOptimizedRouteUseCase,
SaveRouteUseCase,
{
provide: 'IMapService',
useClass: GoogleMapService,
},
{
provide: 'IRouteRepository',
useClass: TypeOrmRouteRepository,
},
],
exports: ['IRouteRepository', 'IMapService', PassportModule],
})
export class MapModule {}
43 changes: 43 additions & 0 deletions src/modules/maps/repos/implementations/typeorm.route.repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Injectable } from '@nestjs/common';
import { IRouteRepository } from '../route.repository.interface';
import { EntityManager } from 'typeorm';
import { Route } from '@modules/maps/domain/route';
import { RouteEntity } from '@database/typeorm/entities/route.entity';
import { RouteMap } from '@modules/maps/domain/mappers/route/route.mapper';

@Injectable()
export class TypeOrmRouteRepository implements IRouteRepository {
constructor(private readonly model: EntityManager) {}

async exists(name: string, userId: string): Promise<boolean> {
const routeModel = this.model.getRepository(RouteEntity);

const foundRoute = await routeModel.findOneBy({
name: name,
userId: userId,
});

return !!foundRoute === true;
}

async save(route: Route): Promise<void> {
const routeModel = this.model.getRepository(RouteEntity);

const routeExists = await this.exists(route.name, route.userId);
if (!routeExists) {
const rawRoute = await RouteMap.toPersistence(route);
await routeModel.save(rawRoute);
}

return;
}

async findOneBy(options: Record<string, any>): Promise<Route> {
const routeModel = this.model.getRepository(RouteEntity);

const foundRoute = await routeModel.findOneBy(options);

if (!foundRoute) return null;
return RouteMap.toDomain(foundRoute);
}
}
7 changes: 7 additions & 0 deletions src/modules/maps/repos/route.repository.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Route } from '../domain/route';

export interface IRouteRepository {
exists(name: string, userId: string): Promise<boolean>;
save(route: Route): Promise<void>;
findOneBy(options: Record<string, any>): Promise<Route>;
}
5 changes: 5 additions & 0 deletions src/modules/maps/usecases/save-route/save-route.command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { SaveRouteRequestDto } from './save-route.dto';

export class SaveRouteCommand {
constructor(public readonly saveRouteDto: SaveRouteRequestDto) {}
}
73 changes: 73 additions & 0 deletions src/modules/maps/usecases/save-route/save-route.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { ApiProperty } from '@nestjs/swagger';
import { MapProvidersEnum } from '@shared/enum/map-providers.enum';
import { Type } from 'class-transformer';
import {
IsArray,
IsEnum,
IsNotEmpty,
IsOptional,
IsString,
IsUUID,
ValidateNested,
} from 'class-validator';

export class PathDisplayInput {
@IsString()
overview_polyline?: string;

@IsArray()
input_coordinate?: [number, number][];

@IsArray()
waypoint_order?: number[];
}

export class SaveRouteRequestDto {
@ApiProperty({
type: String,
example: 'Route 1',
})
@IsString()
@IsNotEmpty()
name: string;

@ApiProperty({
type: PathDisplayInput,
example: {
overview_polyline:
'g|caBqonsS`{RlfI|~^wcGbvXcr\\j~^qsZnvNkoQ`sHevNnlR{mMtne@{`D`sIcxNhdYmxJpwd@clWfqSs_Bxqf@jrFpdQc{IfmQnTxbn@nYbhh@ibPfic@gxH~eDwbHpjI`FjuJ`pDptI{}ChaKjvDzdKwxFphWcwCf|WsiHh}_@omE|b\\dcZjxh@zwPdrYu`LzcX|nV~yJ`wApaO}`In|QnxEhtOe~AbcXtpBdh_@`uOn{Sb~Nvp\\nzGpmMvyg@`Bl_m@~jSzkl@v~b@bje@dtNpuPvcEd}X_x@jgk@weA~jc@toDvjl@z~L`w',
input_coordinate: [
[16.0815, 108.2138],
[16.4661, 107.5847],
[10.77, 106.7043],
[21.0396, 105.855],
[19.8206, 105.7687],
[20.4212, 106.1652],
[10.0342, 105.7834],
],
waypoint_order: [0, 4, 1, 3, 2],
},
})
@IsOptional()
@ValidateNested()
@Type(() => PathDisplayInput)
pathDisplayInput?: PathDisplayInput;

@ApiProperty({
type: String,
example: 'GoogleMaps',
})
@IsString()
@IsNotEmpty()
@IsEnum(MapProvidersEnum)
provider: MapProvidersEnum;

@ApiProperty({
type: String,
example: '18c1e2d6-23e6-4f75-980f-b1339hd328',
})
@IsString()
@IsUUID()
@IsNotEmpty()
userId: string;
}
10 changes: 10 additions & 0 deletions src/modules/maps/usecases/save-route/save-route.errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Result } from '@core/logic/errors-handler';
import { UseCaseError } from '@core/logic/usecase-error';

export class DuplicateRouteName extends Result<UseCaseError> {
constructor(routeName: string) {
super(false, {
message: `Route name "${routeName}" already exists`,
});
}
}
8 changes: 8 additions & 0 deletions src/modules/maps/usecases/save-route/save-route.response.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Either, Result } from '@core/logic/errors-handler';
import { UnexpectedError } from '@core/logic/application-error';
import { DuplicateRouteName } from './save-route.errors';

export type SaveRouteResponse = Either<
DuplicateRouteName | UnexpectedError,
Result<void>
>;
Loading

0 comments on commit 8975aa7

Please sign in to comment.