Skip to content

Commit

Permalink
feat: add retrieve cars planning
Browse files Browse the repository at this point in the history
  • Loading branch information
haroldcohen committed Oct 18, 2023
1 parent ed685f5 commit 4643b75
Show file tree
Hide file tree
Showing 11 changed files with 131 additions and 77 deletions.
4 changes: 3 additions & 1 deletion src/core/domain/car/interfaces/repositories/read.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,12 @@ export default interface CarReadRepositoryInterface {
* @param startDate
* @param endDate
* @param cursor
* @param limit
*/
getCarsPlanning({
startDate,
endDate,
cursor,
}: { startDate: Date, endDate: Date, cursor: DecodedCursor }): Promise<CarsPlanningDTO>;
limit,
}: { startDate: Date, endDate: Date, cursor: DecodedCursor, limit: number }): Promise<CarsPlanningDTO>;
}
3 changes: 1 addition & 2 deletions src/core/domain/car/outputBoundaries/outputBoundary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,11 @@ export type CarRentalPlanningDTO = {
}

export type CarPlanningDTO = {
licensePlate: string;
rentals: CarRentalPlanningDTO[];
};

type Cursor = {
nextPage: null;
nextPage: null | string;
}

type CarPlanningGroupType = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import UserIsNotAuthorizedToRetrieveCarsPlanningError
from "src/core/useCases/car/query/retrieveCarsPlanning/exceptions/notAuthorized";
import PermissionsGatewayInterface from "src/core/useCases/common/interfaces/gateways/permissions";
import UserPermissionsProfile from "src/core/useCases/common/permissions/types/userPermissionsProfile";

export default class RetrieveCarsPlanningAuthorizer {
private readonly permissionsGateway: PermissionsGatewayInterface;
Expand All @@ -10,8 +9,9 @@ export default class RetrieveCarsPlanningAuthorizer {
this.permissionsGateway = permissionsGateway;
}

async userMayRetrieveCarsPlanning(): Promise<void> {
const permissions = await this.permissionsGateway.getUserPermissions();
async authorize({actorId}: { actorId: string }): Promise<void> {
const permissions = await this.permissionsGateway.getUserPermissions({userId: actorId});

if (permissions.length === 0) {
throw new UserIsNotAuthorizedToRetrieveCarsPlanningError();
}
Expand Down
28 changes: 8 additions & 20 deletions src/core/useCases/car/query/retrieveCarsPlanning/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ import CarsPlanningDTO from "src/core/domain/car/outputBoundaries/outputBoundary
import CarReadRepositoryInterface from "src/core/domain/car/interfaces/repositories/read";
import RetrieveCarsPlanningQuery from "src/core/useCases/car/query/retrieveCarsPlanning/types/query";
import RetrieveCarsPlanningAuthorizer from "src/core/useCases/car/query/retrieveCarsPlanning/authorizer";
import DecodedCursor from "src/core/domain/common/types/cursor";
import {decodeCursor} from "src/core/useCases/common/cursors/encodeDecode";

export default class RetrieveCarsPlanning {
private readonly carReadRepository;
private readonly carReadRepository: CarReadRepositoryInterface;

private readonly authorizer;
private readonly authorizer: RetrieveCarsPlanningAuthorizer;

constructor({
carReadRepository,
Expand All @@ -18,27 +18,15 @@ export default class RetrieveCarsPlanning {
}

async execute(query: RetrieveCarsPlanningQuery): Promise<CarsPlanningDTO> {
await this.authorizer.userMayRetrieveCarsPlanning();
await this.authorizer.authorize({
actorId: query.actor.id,
});

return await this.carReadRepository.getCarsPlanning({

Check failure on line 25 in src/core/useCases/car/query/retrieveCarsPlanning/handler.ts

View workflow job for this annotation

GitHub Actions / lint (20.x)

Redundant use of `await` on a return value
startDate: query.startDate,
endDate: query.endDate,
cursor: this.decodeCursor(query.cursor),
limit: query.limit,
cursor: decodeCursor(query.cursor),
});
}

private decodeCursor(cursor: null | string): DecodedCursor {
if (cursor === null) {
return {
order: 'gte',
address: ''
}
}
const cursorMatch = atob(cursor).match(new RegExp('^(?<order>(next|prev)___)(?<address>[\\w\'-]+)$')) as RegExpMatchArray;
const groups = cursorMatch.groups as any;
return {
order: groups.order == 'next' ? 'gte' : 'lte',
address: groups.address as string,
}
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
type Actor = {
id: string;
}

type RetrieveCarsPlanningQuery = {
startDate: Date;
endDate: Date;
limit: number;
cursor: null | string;
cursor: string;
actor: Actor;
};

export default RetrieveCarsPlanningQuery;
30 changes: 30 additions & 0 deletions src/core/useCases/common/cursors/encodeDecode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import DecodedCursor from "src/core/domain/common/types/cursor";

/**
* Encodes a cursor address to a base64 format.
*
* @param cursor The cursor address to encode
* @param order The order of the cursor (gte for next, lte for prev)
*/
export const encodeCursor = (cursor: string, order: string): string => {
if (cursor === ''){
return '';
}
return btoa(`${order == 'gte' ? 'next' : 'prev'}___${cursor}`);

Check failure on line 13 in src/core/useCases/common/cursors/encodeDecode.ts

View workflow job for this annotation

GitHub Actions / lint (20.x)

Expected '===' and instead saw '=='
}

export const decodeCursor = (cursor: string): DecodedCursor => {
if (cursor === '') {
return {
order: 'gte',
address: ''
}
}
const cursorMatch = atob(cursor).match(new RegExp('^(?<order>(next|prev))___(?<address>[\\w\'-]+)$')) as RegExpMatchArray;

Check failure on line 23 in src/core/useCases/common/cursors/encodeDecode.ts

View workflow job for this annotation

GitHub Actions / lint (20.x)

Use a regular expression literal instead of the 'RegExp' constructor
const groups = cursorMatch.groups as any;

return {
order: groups.order == 'next' ? 'gte' : 'lte',

Check failure on line 27 in src/core/useCases/common/cursors/encodeDecode.ts

View workflow job for this annotation

GitHub Actions / lint (20.x)

Expected '===' and instead saw '=='
address: groups.address as string,
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import UserPermissionsProfile from "src/core/useCases/common/permissions/types/userPermissionsProfile";

export default interface PermissionsGatewayInterface {
getUserPermissions(): Promise<UserPermissionsProfile[]>;
getUserPermissions({userId}: { userId: string }): Promise<UserPermissionsProfile[]>;
}
54 changes: 32 additions & 22 deletions src/driven/repositories/inMemory/car/read.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import InMemoryCarModel from '../carModel/carModel.entity';
import UnavailableCarError from 'src/core/domain/car/errors/unavailable';
import CarsPlanningDTO from "src/core/domain/car/outputBoundaries/outputBoundary";
import DecodedCursor from "src/core/domain/common/types/cursor";
import {encodeCursor} from "src/core/useCases/common/cursors/encodeDecode";

@injectable()
export default class InMemoryCarReadRepository implements CarReadRepositoryInterface {
Expand Down Expand Up @@ -74,44 +75,53 @@ export default class InMemoryCarReadRepository implements CarReadRepositoryInter
})
}

findNextCursor(cars: InMemoryCar[]): string {
const nextCar = _.find(
this.unitOfWork.cars,
inMemoryCar => inMemoryCar.licensePlate > cars[cars.length - 1].licensePlate
);
if (nextCar === undefined) {
return "";
}

return cars[cars.length - 1].licensePlate;
}

async getCarsPlanning({
startDate,
endDate,
cursor,
}: { startDate: Date, endDate: Date, cursor: DecodedCursor }): Promise<CarsPlanningDTO> {
const select = (cars: InMemoryCar[], limit: number, cursor: DecodedCursor): InMemoryCar[] => {
const results: InMemoryCar[] = [];
const sorted = _.orderBy(cars, ['licensePlate'], ['asc']);
const filtered = _.filter(
sorted,
inMemoryCar => cursor.order === 'gte' ? inMemoryCar.licensePlate >= cursor.address : inMemoryCar.licensePlate >= cursor.address
);
_.forEach(filtered, (car) => {
if (results.length === limit) {
return false;
}
results.push(car);
});

return results;
}
const retrievedCars = select(this.unitOfWork.cars, 5, cursor);
limit,
}: { startDate: Date, endDate: Date, cursor: DecodedCursor, limit: number }): Promise<CarsPlanningDTO> {
const retrievedCars: InMemoryCar[] = [];
const sorted = _.orderBy(this.unitOfWork.cars, ['licensePlate'], ['asc']);
const filtered = _.filter(
sorted,
inMemoryCar => cursor.order === 'gte' ? inMemoryCar.licensePlate >= cursor.address : inMemoryCar.licensePlate >= cursor.address
);
_.forEach(filtered, (car) => {

Check failure on line 102 in src/driven/repositories/inMemory/car/read.ts

View workflow job for this annotation

GitHub Actions / lint (20.x)

Expected to return a value at the end of arrow function
if (retrievedCars.length === limit) {
return false;
}
retrievedCars.push(car);
});
const planning = {
cars: {},
cursor: {
nextPage: null,
nextPage: encodeCursor(this.findNextCursor(retrievedCars), cursor.order),
},
} as CarsPlanningDTO;
for (const retrievedCar of retrievedCars) {
const retrievedCarRentals: InMemoryCarRental[] = _.filter(
this.unitOfWork.carRentals,
inMemoryCarRental =>
inMemoryCarRental.carId === retrievedCar.id
&& inMemoryCarRental.pickupDateTime >= startDate
&& inMemoryCarRental.pickupDateTime <= endDate,
&& (
(inMemoryCarRental.pickupDateTime >= startDate && inMemoryCarRental.pickupDateTime <= endDate)
|| (inMemoryCarRental.dropOffDateTime > startDate && inMemoryCarRental.pickupDateTime <= startDate)
)
);
planning.cars[retrievedCar.id] = {
licensePlate: retrievedCar.licensePlate,
rentals: []
}
for (const retrievedCarRental of retrievedCarRentals) {
Expand Down
10 changes: 5 additions & 5 deletions tests/common/stubs/gateways/permissions/allow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ import PermissionsGatewayInterface from "src/core/useCases/common/interfaces/gat
import UserPermissionsProfile from "src/core/useCases/common/permissions/types/userPermissionsProfile";

export default class PermissionsStubGateway implements PermissionsGatewayInterface {
constructor(permissionsProfileToProvide: UserPermissionsProfile[]) {
private readonly permissionsToProvide: UserPermissionsProfile[];

constructor(permissionsToProvide: UserPermissionsProfile[]) {
this.permissionsToProvide = permissionsToProvide;
}

// @ts-ignore
async getUserPermissions(): Promise<UserPermissionsProfile[]> {
return [
{}
]
return this.permissionsToProvide;
}

}
5 changes: 2 additions & 3 deletions tests/unit/car/query/retrieveCarsPlanning/beforeEach.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,16 @@ import CarsPlanningDTO from "src/core/domain/car/outputBoundaries/outputBoundary
import DateParser from "tests/utils/dateParser";
import {CarTestCaseEntry} from "tests/unit/utils/testCase.types";

export const buildExpectedCarsPlanning = (cars: CarTestCaseEntry[]): CarsPlanningDTO => {
export const buildExpectedCarsPlanning = (cars: CarTestCaseEntry[], nextCursor: string, prevCursor: null | string): CarsPlanningDTO => {
const dateParser: DateParser = container.resolve("DateParser");
const expectedPlanning: CarsPlanningDTO = {
cars: {},
cursor: {
nextPage: null,
nextPage: nextCursor,
}
}
for (const carTestCaseEntry of cars) {
expectedPlanning.cars[carTestCaseEntry.id] = {
licensePlate: carTestCaseEntry.licensePlate !== undefined ? carTestCaseEntry.licensePlate : 'AA-123-AA',
rentals: []
}
for (const rentalEntry of carTestCaseEntry.rentals) {
Expand Down
Loading

0 comments on commit 4643b75

Please sign in to comment.