add swagger + catalog controller
This commit is contained in:
parent
171d670629
commit
d4423d9e33
|
|
@ -13,6 +13,8 @@
|
|||
"@nestjs/core": "^11.0.1",
|
||||
"@nestjs/mapped-types": "^2.1.0",
|
||||
"@nestjs/platform-express": "^11.0.1",
|
||||
"@nestjs/swagger": "^11.2.3",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.3",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1"
|
||||
|
|
@ -2054,6 +2056,12 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@microsoft/tsdoc": {
|
||||
"version": "0.16.0",
|
||||
"resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.16.0.tgz",
|
||||
"integrity": "sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@napi-rs/wasm-runtime": {
|
||||
"version": "0.2.12",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz",
|
||||
|
|
@ -2509,6 +2517,39 @@
|
|||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/swagger": {
|
||||
"version": "11.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-11.2.3.tgz",
|
||||
"integrity": "sha512-a0xFfjeqk69uHIUpP8u0ryn4cKuHdra2Ug96L858i0N200Hxho+n3j+TlQXyOF4EstLSGjTfxI1Xb2E1lUxeNg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@microsoft/tsdoc": "0.16.0",
|
||||
"@nestjs/mapped-types": "2.1.0",
|
||||
"js-yaml": "4.1.1",
|
||||
"lodash": "4.17.21",
|
||||
"path-to-regexp": "8.3.0",
|
||||
"swagger-ui-dist": "5.30.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@fastify/static": "^8.0.0",
|
||||
"@nestjs/common": "^11.0.1",
|
||||
"@nestjs/core": "^11.0.1",
|
||||
"class-transformer": "*",
|
||||
"class-validator": "*",
|
||||
"reflect-metadata": "^0.1.12 || ^0.2.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@fastify/static": {
|
||||
"optional": true
|
||||
},
|
||||
"class-transformer": {
|
||||
"optional": true
|
||||
},
|
||||
"class-validator": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/testing": {
|
||||
"version": "11.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-11.1.9.tgz",
|
||||
|
|
@ -2638,6 +2679,13 @@
|
|||
"url": "https://opencollective.com/pkgr"
|
||||
}
|
||||
},
|
||||
"node_modules/@scarf/scarf": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz",
|
||||
"integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@sinclair/typebox": {
|
||||
"version": "0.34.41",
|
||||
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz",
|
||||
|
|
@ -3981,7 +4029,6 @@
|
|||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||
"dev": true,
|
||||
"license": "Python-2.0"
|
||||
},
|
||||
"node_modules/array-timsort": {
|
||||
|
|
@ -4471,6 +4518,12 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/class-transformer": {
|
||||
"version": "0.5.1",
|
||||
"resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz",
|
||||
"integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/class-validator": {
|
||||
"version": "0.14.3",
|
||||
"resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.3.tgz",
|
||||
|
|
@ -7249,7 +7302,6 @@
|
|||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
||||
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1"
|
||||
|
|
@ -7432,7 +7484,6 @@
|
|||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.memoize": {
|
||||
|
|
@ -9192,6 +9243,15 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/swagger-ui-dist": {
|
||||
"version": "5.30.2",
|
||||
"resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.30.2.tgz",
|
||||
"integrity": "sha512-HWCg1DTNE/Nmapt+0m2EPXFwNKNeKK4PwMjkwveN/zn1cV2Kxi9SURd+m0SpdcSgWEK/O64sf8bzXdtUhigtHA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@scarf/scarf": "=1.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/symbol-observable": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz",
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@
|
|||
"@nestjs/core": "^11.0.1",
|
||||
"@nestjs/mapped-types": "^2.1.0",
|
||||
"@nestjs/platform-express": "^11.0.1",
|
||||
"@nestjs/swagger": "^11.2.3",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.3",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1"
|
||||
|
|
|
|||
|
|
@ -2,9 +2,10 @@ import { Module } from '@nestjs/common';
|
|||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
import { YachtModule } from './yacht/yacht.module';
|
||||
import { CatalogModule } from './catalog/catalog.module';
|
||||
|
||||
@Module({
|
||||
imports: [YachtModule],
|
||||
imports: [YachtModule, CatalogModule],
|
||||
controllers: [AppController],
|
||||
providers: [AppService],
|
||||
})
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { CatalogController } from './catalog.controller';
|
||||
|
||||
describe('CatalogController', () => {
|
||||
let controller: CatalogController;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [CatalogController],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<CatalogController>(CatalogController);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(controller).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
import { Controller, Get, Query } from '@nestjs/common';
|
||||
import { CatalogService } from './catalog.service';
|
||||
import { CatalogResponseDto } from './dto/catalog-response.dto';
|
||||
import { CatalogItemDto } from './dto/catalog-item.dto';
|
||||
import { CatalogParamsDto } from './dto/catalog-params.dto';
|
||||
import { ApiQuery, ApiResponse, ApiOperation } from '@nestjs/swagger';
|
||||
|
||||
@Controller('catalog')
|
||||
export class CatalogController {
|
||||
constructor(private readonly catalogService: CatalogService) {}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: 'Get catalog with filters, search and sorting' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Catalog items with filters applied',
|
||||
type: CatalogResponseDto,
|
||||
})
|
||||
@ApiQuery({
|
||||
name: 'params',
|
||||
required: false,
|
||||
description: 'JSON string of filter parameters',
|
||||
type: String,
|
||||
})
|
||||
async getCatalog(
|
||||
@Query('params') paramsString?: string,
|
||||
): Promise<CatalogResponseDto> {
|
||||
let params: CatalogParamsDto = {};
|
||||
|
||||
if (paramsString) {
|
||||
try {
|
||||
params = JSON.parse(decodeURIComponent(paramsString));
|
||||
} catch (error) {
|
||||
params = {};
|
||||
}
|
||||
}
|
||||
|
||||
return this.catalogService.getCatalog(params);
|
||||
}
|
||||
|
||||
@Get('all')
|
||||
@ApiOperation({ summary: 'Get all catalog items without filters' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'All catalog items',
|
||||
type: [CatalogItemDto],
|
||||
})
|
||||
async getAllCatalogItems(): Promise<CatalogItemDto[]> {
|
||||
return this.catalogService.getAllCatalogItems();
|
||||
}
|
||||
|
||||
@Get('filtered')
|
||||
@ApiOperation({ summary: 'Get catalog with explicit query parameters' })
|
||||
async getFilteredCatalog(
|
||||
@Query() params: CatalogParamsDto,
|
||||
): Promise<CatalogResponseDto> {
|
||||
return this.catalogService.getCatalog(params);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { CatalogController } from './catalog.controller';
|
||||
import { CatalogService } from './catalog.service';
|
||||
|
||||
@Module({
|
||||
controllers: [CatalogController],
|
||||
providers: [CatalogService],
|
||||
exports: [CatalogService],
|
||||
})
|
||||
export class CatalogModule {}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { CatalogService } from './catalog.service';
|
||||
|
||||
describe('CatalogService', () => {
|
||||
let service: CatalogService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [CatalogService],
|
||||
}).compile();
|
||||
|
||||
service = module.get<CatalogService>(CatalogService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { CatalogItemDto } from './dto/catalog-item.dto';
|
||||
import { CatalogParamsDto } from './dto/catalog-params.dto';
|
||||
import { CatalogResponseDto } from './dto/catalog-response.dto';
|
||||
|
||||
@Injectable()
|
||||
export class CatalogService {
|
||||
private catalogItems: CatalogItemDto[] = [
|
||||
// SAMPLE DATA
|
||||
{
|
||||
id: 1,
|
||||
name: 'Item 1',
|
||||
length: 10,
|
||||
speed: 100,
|
||||
minCost: 50,
|
||||
hasQuickRent: true,
|
||||
pictureUrl: 'https://example.com/item1.jpg',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Item 2',
|
||||
length: 10,
|
||||
speed: 100,
|
||||
minCost: 50,
|
||||
hasQuickRent: false,
|
||||
pictureUrl: 'https://example.com/item1.jpg',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Item 3',
|
||||
length: 10,
|
||||
speed: 100,
|
||||
minCost: 50,
|
||||
hasQuickRent: true,
|
||||
pictureUrl: 'https://example.com/item1.jpg',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'Item 4',
|
||||
length: 10,
|
||||
speed: 100,
|
||||
minCost: 50,
|
||||
hasQuickRent: true,
|
||||
pictureUrl: 'https://example.com/item1.jpg',
|
||||
},
|
||||
];
|
||||
|
||||
async getCatalog(params?: CatalogParamsDto): Promise<CatalogResponseDto> {
|
||||
let filteredItems = [...this.catalogItems];
|
||||
|
||||
if (params?.search) {
|
||||
const searchTerm = params.search.toLowerCase();
|
||||
|
||||
filteredItems = filteredItems.filter((item) =>
|
||||
item.name.toLowerCase().includes(searchTerm),
|
||||
);
|
||||
}
|
||||
|
||||
if (params?.filter?.price) {
|
||||
const { minvalue, maxvalue } = params.filter.price;
|
||||
|
||||
filteredItems = filteredItems.filter((item) => {
|
||||
if (minvalue !== undefined && item.minCost < minvalue) return false;
|
||||
if (maxvalue !== undefined && item.minCost > maxvalue) return false;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
if (params?.sort?.field) {
|
||||
const { field, direction = 'asc' } = params.sort;
|
||||
|
||||
filteredItems.sort((a, b) => {
|
||||
let valueA = a[field];
|
||||
let valueB = b[field];
|
||||
|
||||
if (typeof valueA === 'string') valueA = valueA.toLowerCase();
|
||||
if (typeof valueB === 'string') valueB = valueB.toLowerCase();
|
||||
|
||||
if (valueA < valueB) return direction === 'asc' ? -1 : 1;
|
||||
if (valueA > valueB) return direction === 'asc' ? 1 : -1;
|
||||
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
items: filteredItems,
|
||||
total: filteredItems.length,
|
||||
filters: params?.filter,
|
||||
sort: params?.sort,
|
||||
search: params?.search,
|
||||
};
|
||||
}
|
||||
|
||||
async getAllCatalogItems(): Promise<CatalogItemDto[]> {
|
||||
return this.catalogItems;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
export class CatalogItemDto {
|
||||
id?: number;
|
||||
name: string;
|
||||
length: number;
|
||||
speed: number;
|
||||
minCost: number;
|
||||
hasQuickRent: boolean;
|
||||
pictureUrl: string;
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
import {
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsNumber,
|
||||
ValidateNested,
|
||||
IsEnum,
|
||||
} from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export enum SortDirection {
|
||||
ASC = 'asc',
|
||||
DESC = 'desc',
|
||||
}
|
||||
|
||||
export class SortParams {
|
||||
@ApiProperty({ required: false, enum: SortDirection })
|
||||
@IsOptional()
|
||||
@IsEnum(SortDirection)
|
||||
direction?: SortDirection;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
field?: string;
|
||||
}
|
||||
|
||||
export class PriceFilter {
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
minvalue?: number;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
maxvalue?: number;
|
||||
}
|
||||
|
||||
export class FilterParams {
|
||||
@ApiProperty({ required: false, type: PriceFilter })
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => PriceFilter)
|
||||
price?: PriceFilter;
|
||||
}
|
||||
|
||||
export class CatalogParamsDto {
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
search?: string;
|
||||
|
||||
@ApiProperty({ required: false, type: SortParams })
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => SortParams)
|
||||
sort?: SortParams;
|
||||
|
||||
@ApiProperty({ required: false, type: FilterParams })
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => FilterParams)
|
||||
filter?: FilterParams;
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import { CatalogItemDto } from './catalog-item.dto';
|
||||
|
||||
export class CatalogResponseDto {
|
||||
items: CatalogItemDto[];
|
||||
total: number;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
filters?: any;
|
||||
sort?: any;
|
||||
search?: string;
|
||||
}
|
||||
13
src/main.ts
13
src/main.ts
|
|
@ -1,8 +1,19 @@
|
|||
import { NestFactory } from '@nestjs/core';
|
||||
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
|
||||
import { AppModule } from './app.module';
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule);
|
||||
await app.listen(process.env.PORT ?? 3000);
|
||||
|
||||
const config = new DocumentBuilder()
|
||||
.setTitle('Travelmarine backend')
|
||||
.setDescription('Backend API for Travelmarine service')
|
||||
.setVersion('0.1')
|
||||
.build();
|
||||
|
||||
const documentFactory = () => SwaggerModule.createDocument(app, config);
|
||||
SwaggerModule.setup('api', app, documentFactory);
|
||||
|
||||
await app.listen(process.env.PORT ?? 8001);
|
||||
}
|
||||
bootstrap();
|
||||
|
|
|
|||
|
|
@ -1,25 +0,0 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { INestApplication } from '@nestjs/common';
|
||||
import request from 'supertest';
|
||||
import { App } from 'supertest/types';
|
||||
import { AppModule } from './../src/app.module';
|
||||
|
||||
describe('AppController (e2e)', () => {
|
||||
let app: INestApplication<App>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const moduleFixture: TestingModule = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
}).compile();
|
||||
|
||||
app = moduleFixture.createNestApplication();
|
||||
await app.init();
|
||||
});
|
||||
|
||||
it('/ (GET)', () => {
|
||||
return request(app.getHttpServer())
|
||||
.get('/')
|
||||
.expect(200)
|
||||
.expect('Hello World!');
|
||||
});
|
||||
});
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
{
|
||||
"moduleFileExtensions": ["js", "json", "ts"],
|
||||
"rootDir": ".",
|
||||
"testEnvironment": "node",
|
||||
"testRegex": ".e2e-spec.ts$",
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": "ts-jest"
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue