-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add basic crud operations for locations
Refs: #18
- Loading branch information
Showing
20 changed files
with
884 additions
and
94 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,5 @@ | ||
{ | ||
"singleQuote": true, | ||
"trailingComma": "all" | ||
"trailingComma": "all", | ||
"printWidth": 150 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import { LocationDto } from './location.dto'; | ||
|
||
export class UpdateLocationDto extends LocationDto {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
134 changes: 134 additions & 0 deletions
134
workspaces/api/src/location/location.controller.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 {} |
Oops, something went wrong.