Skip to content

Commit

Permalink
feat: add basic crud operations for locations
Browse files Browse the repository at this point in the history
Refs: #18
  • Loading branch information
resah committed Jun 20, 2023
1 parent 455e4a4 commit b5a8e6a
Show file tree
Hide file tree
Showing 20 changed files with 884 additions and 94 deletions.
3 changes: 2 additions & 1 deletion workspaces/api/.prettierrc
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"singleQuote": true,
"trailingComma": "all"
"trailingComma": "all",
"printWidth": 150
}
4 changes: 4 additions & 0 deletions workspaces/api/src/location/dto/create-location.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { OmitType } from '@nestjs/swagger';
import { LocationDto } from './location.dto';

export class CreateLocationDto extends OmitType(LocationDto, ['id'] as const) {}
28 changes: 28 additions & 0 deletions workspaces/api/src/location/dto/location.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { ApiProperty } from '@nestjs/swagger';

export class LocationDto {
@ApiProperty()
id: string;

@ApiProperty()
name: string;

@ApiProperty()
shortName: string;

@ApiProperty()
description: string;

@ApiProperty()
image: string;

@ApiProperty()
width: number;

@ApiProperty()
height: number;

constructor(partial: Partial<LocationDto>) {
Object.assign(this, partial);
}
}
3 changes: 3 additions & 0 deletions workspaces/api/src/location/dto/update-location.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { LocationDto } from './location.dto';

export class UpdateLocationDto extends LocationDto {}
4 changes: 2 additions & 2 deletions workspaces/api/src/location/entities/location.entity.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import { Location } from './location.entity';
describe('Location', () => {
describe('getType', () => {
it('should provide database entity type', () => {
const marker = new Location();
const location = new Location();

expect(marker.getType()).toBe('/locations');
expect(location.getType()).toBe('/locations');
});
});
});
3 changes: 3 additions & 0 deletions workspaces/api/src/location/entities/location.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import { EntityType } from '../../persistence/entities/entity.type';
export class Location extends Entity {
static TYPE: EntityType = '/locations';

name: string;
shortName: string;
description: string;
image: string;
width: number;
height: number;
Expand Down
134 changes: 134 additions & 0 deletions workspaces/api/src/location/location.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { LocationController } from './location.controller';
import { LocationService } from './location.service';
import { LocationDto } from './dto/location.dto';
import { CreateLocationDto } from './dto/create-location.dto';
import { UpdateLocationDto } from './dto/update-location.dto';
import { UploadDto } from '../persistence/dto/upload.dto';
import { Response } from 'express';

describe('LocationController', () => {
let controller: LocationController;
const locationService: LocationService = {} as LocationService;

beforeEach(async () => {
controller = new LocationController(locationService);
});

describe('create', () => {
it('should return newly created location', async () => {
const expectedResult = new LocationDto({});
const result: Promise<LocationDto> = new Promise<LocationDto>(
(resolve) => {
resolve(expectedResult);
},
);
locationService.create = vi.fn();
vi.spyOn(locationService, 'create').mockImplementation(() => result);

const actual = await controller.create({} as CreateLocationDto);

expect(actual).toBe(expectedResult);
});
});

describe('findAll', () => {
it('should return an array of locations', async () => {
const expectedResult = [new LocationDto({}), new LocationDto({})];
const result: Promise<LocationDto[]> = new Promise<LocationDto[]>(
(resolve) => {
resolve(expectedResult);
},
);
locationService.findAll = vi.fn();
vi.spyOn(locationService, 'findAll').mockImplementation(() => result);

const actual = await controller.findAll();

expect(actual).toBe(expectedResult);
});
});

describe('findOne', () => {
it('should return a single location', async () => {
const expectedResult = new LocationDto({});
const result: Promise<LocationDto> = new Promise<LocationDto>(
(resolve) => {
resolve(expectedResult);
},
);
locationService.findOne = vi.fn();
vi.spyOn(locationService, 'findOne').mockImplementation(() => result);

const actual = await controller.findOne('abc-123');

expect(actual).toBe(expectedResult);
});
});

describe('update', () => {
it('should return the updated location', async () => {
const expectedResult = new LocationDto({});
const result: Promise<LocationDto> = new Promise<LocationDto>(
(resolve) => {
resolve(expectedResult);
},
);
locationService.update = vi.fn();
vi.spyOn(locationService, 'update').mockImplementation(() => result);

const actual = await controller.update(
'abc-123',
{} as UpdateLocationDto,
);

expect(actual).toBe(expectedResult);
});
});

describe('delete', () => {
it('should call location deletion', async () => {
locationService.delete = vi.fn();
vi.spyOn(locationService, 'delete');

await controller.delete('abc-123');

expect(locationService.delete).toHaveBeenCalledTimes(1);
});
});

describe('getImage', () => {
it('should retrieve image', async () => {
const mockResponse = {
type: vi.fn(),
end: vi.fn(),
} as unknown as Response;
const mockImage = Buffer.from('abc');
locationService.getImage = vi.fn();
vi.spyOn(locationService, 'getImage').mockImplementation(() => {
return new Promise<Buffer>((resolve) => resolve(mockImage));
});

await controller.getImage('abc-123', mockResponse);

expect(locationService.getImage).toHaveBeenCalledTimes(1);
expect(mockResponse.type).toHaveBeenCalledWith('png');
expect(mockResponse.end).toHaveBeenCalledWith(mockImage, 'binary');
});
});

describe('uploadImage', () => {
it('should upload image', async () => {
locationService.uploadImage = vi.fn();
vi.spyOn(locationService, 'uploadImage');

await controller.uploadImage(
'abc-123',
new UploadDto(),
{} as Express.Multer.File,
);

expect(locationService.uploadImage).toHaveBeenCalledTimes(1);
});
});
});
143 changes: 143 additions & 0 deletions workspaces/api/src/location/location.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import {
Controller,
Get,
Post,
Body,
Put,
Param,
Delete,
UploadedFile,
ParseUUIDPipe,
ParseFilePipe,
MaxFileSizeValidator,
FileTypeValidator,
Res,
} from '@nestjs/common';
import {
ApiExtraModels,
ApiOperation,
ApiParam,
ApiProduces,
ApiResponse,
ApiTags,
getSchemaPath,
} from '@nestjs/swagger';
import { Response } from 'express';
import { CreateLocationDto } from './dto/create-location.dto';
import { UpdateLocationDto } from './dto/update-location.dto';
import { LocationDto } from './dto/location.dto';
import { UploadDto } from '../persistence/dto/upload.dto';
import { ApiImageDownload } from '../persistence/decorators/api-image-download.decorator';
import { ApiUpload } from '../persistence/decorators/api-upload.decorator';
import { LocationService } from './location.service';

@Controller(['locations'])
@ApiTags('locations')
@ApiExtraModels(LocationDto)
export class LocationController {
private static readonly MAX_FILE_SIZE = Math.pow(1024, 2);

constructor(private readonly locationService: LocationService) {}

@Post()
@ApiOperation({
summary: 'Create a new location',
})
create(@Body() createLocationDto: CreateLocationDto) {
return this.locationService.create(createLocationDto);
}

@Get()
@ApiOperation({
summary: 'List all locations',
})
@ApiResponse({
description: 'List of location objects',
schema: {
type: 'array',
items: {
$ref: getSchemaPath(LocationDto),
},
},
})
findAll(): Promise<LocationDto[]> {
return this.locationService.findAll();
}

@Get(':id')
@ApiOperation({
summary: 'Get a single location by its ID',
})
@ApiParam({ name: 'id', description: 'Marker ID' })
@ApiResponse({
description: 'Marker object',
schema: {
$ref: getSchemaPath(LocationDto),
},
})
async findOne(@Param('id', ParseUUIDPipe) id: string): Promise<LocationDto> {
return await this.locationService.findOne(id);
}

@Put(':id')
@ApiOperation({
summary: 'Update a location',
})
@ApiParam({ name: 'id', description: 'Marker ID' })
update(
@Param('id', ParseUUIDPipe) id: string,
@Body() updateMarkerDto: UpdateLocationDto,
): Promise<LocationDto> {
return this.locationService.update(id, updateMarkerDto);
}

@Delete(':id')
@ApiOperation({
summary: 'Delete a location',
})
@ApiParam({ name: 'id', description: 'Marker ID' })
delete(@Param('id', ParseUUIDPipe) id: string): Promise<void> {
return this.locationService.delete(id);
}

@Get(':id/image')
@ApiOperation({
summary: 'Provides location image',
})
@ApiParam({ name: 'id', description: 'Marker ID' })
@ApiImageDownload()
async getImage(
@Param('id', ParseUUIDPipe) id: string,
@Res()
response: Response,
): Promise<void> {
const buffer = await this.locationService.getImage(id);
response.type('png');
response.end(buffer, 'binary');
}

@Post(':id/image/upload')
@ApiOperation({
summary:
'Upload for a location image; Returns image ID; (This may change in the future)',
})
@ApiUpload('file', LocationController.MAX_FILE_SIZE)
@ApiProduces('text/plain')
@ApiParam({ name: 'id', description: 'Marker ID' })
async uploadImage(
@Param('id', ParseUUIDPipe) id: string,
@Body() uploadDto: UploadDto,
@UploadedFile(
new ParseFilePipe({
fileIsRequired: true,
validators: [
new MaxFileSizeValidator({ maxSize: LocationController.MAX_FILE_SIZE }),
new FileTypeValidator({ fileType: new RegExp('png|jpeg|jpg') }),
],
}),
)
file: Express.Multer.File,
): Promise<string> {
return this.locationService.uploadImage(id, file);
}
}
10 changes: 9 additions & 1 deletion workspaces/api/src/location/location.module.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import { Module } from '@nestjs/common';
import { PersistenceModule } from '../persistence/persistence.module';
import { LocationMapperService } from './utils/location-mapper.service';
import { LocationService } from './location.service';
import { LocationController } from './location.controller';

@Module({})
@Module({
imports: [PersistenceModule],
controllers: [LocationController],
providers: [LocationService, LocationMapperService],
})
export class LocationModule {}
Loading

0 comments on commit b5a8e6a

Please sign in to comment.