diff --git a/.env.example b/.env.example index ff70528..2cc988b 100644 --- a/.env.example +++ b/.env.example @@ -10,4 +10,6 @@ PGADMIN_DEFAULT_EMAIL= PGADMIN_DEFAULT_PASSWORD= # JWT JWT_SECRET= -JWT_EXPIRES_IN=7d \ No newline at end of file +JWT_EXPIRES_IN=7d +# API +API_URL= \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index bd58b98..22e59cc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "api", - "version": "0.0.1", + "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "api", - "version": "0.0.1", + "version": "0.1.0", "license": "UNLICENSED", "dependencies": { "@nestjs/common": "^11.0.1", diff --git a/src/products/dto/find-products.dto.ts b/src/products/dto/find-products.dto.ts index 713105d..924a3dd 100644 --- a/src/products/dto/find-products.dto.ts +++ b/src/products/dto/find-products.dto.ts @@ -1,20 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; - -class UUIDBaseDTO { - @ApiProperty({ example: '123e4567-e89b-12d3-a456-426614174006' }) - id: string; -} - -export class BaseDTO extends UUIDBaseDTO { - @ApiProperty() - createdAt: Date; - - @ApiProperty() - updatedAt: Date; - - @ApiProperty({ example: null }) - deletedAt: Date; -} +import { BaseDTO, UUIDBaseDTO } from 'src/utils/dto/base.dto'; export class ManufacturerDTO extends BaseDTO { @ApiProperty() @@ -24,7 +9,7 @@ export class ManufacturerDTO extends BaseDTO { description: string; } -export class ImagesDTO extends BaseDTO { +export class ImageDTO extends BaseDTO { @ApiProperty() url: string; } @@ -34,7 +19,7 @@ export class LotDTO extends BaseDTO { expirationDate: Date; } -export class CategorieDTO extends UUIDBaseDTO { +export class CategoryDTO extends UUIDBaseDTO { @ApiProperty() name: string; @@ -56,7 +41,7 @@ export class PresentationDTO extends BaseDTO { meansurementUnit: string; } -export class PresentationsDTO extends BaseDTO { +export class ProductPresentationDTO extends BaseDTO { @ApiProperty() price: number; @@ -64,7 +49,7 @@ export class PresentationsDTO extends BaseDTO { presentation: PresentationDTO; } -export class ProductListDTO extends BaseDTO { +export class ProductDTO extends BaseDTO { @ApiProperty() name: string; @@ -80,15 +65,12 @@ export class ProductListDTO extends BaseDTO { @ApiProperty({ type: ManufacturerDTO }) manufacturer: ManufacturerDTO; - @ApiProperty({ type: ImagesDTO }) - images: ImagesDTO[]; - - @ApiProperty({ type: [LotDTO] }) - lot: LotDTO[]; + @ApiProperty({ type: ImageDTO }) + images: ImageDTO[]; - @ApiProperty({ type: [CategorieDTO] }) - categories: CategorieDTO[]; + @ApiProperty({ type: [CategoryDTO] }) + categories: CategoryDTO[]; - @ApiProperty({ type: [PresentationsDTO] }) - presentations: PresentationsDTO[]; + @ApiProperty({ type: [ProductPresentationDTO] }) + presentations: ProductPresentationDTO[]; } diff --git a/src/products/products.controller.ts b/src/products/products.controller.ts index 36cf8e7..9118e63 100644 --- a/src/products/products.controller.ts +++ b/src/products/products.controller.ts @@ -4,14 +4,28 @@ import { Get, ParseIntPipe, Query, + Req, } from '@nestjs/common'; import { ProductsService } from './products.service'; -import { ApiOperation, ApiResponse } from '@nestjs/swagger'; -import { ProductListDTO } from './dto/find-products.dto'; +import { + ApiExtraModels, + ApiOkResponse, + ApiOperation, + getSchemaPath, +} from '@nestjs/swagger'; +import { ProductDTO } from './dto/find-products.dto'; +import { Request } from 'express'; +import { ConfigService } from '@nestjs/config'; +import { getPaginationUrl } from 'src/utils/pagination-urls'; +import { PaginationDTO } from 'src/utils/dto/pagination.dto'; @Controller('product') +@ApiExtraModels(PaginationDTO, ProductDTO) export class ProductsController { - constructor(private productsServices: ProductsService) {} + constructor( + private productsServices: ProductsService, + private configService: ConfigService, + ) {} @Get() @ApiOperation({ @@ -19,15 +33,31 @@ export class ProductsController { description: 'returns all available products (deletedAt is NULL). It will include their images, lots, presentations, manufacturers and categories.', }) - @ApiResponse({ - status: 200, + @ApiOkResponse({ description: 'Products obtained correctly.', - type: [ProductListDTO], + schema: { + allOf: [ + { $ref: getSchemaPath(PaginationDTO) }, + { + properties: { + results: { + type: 'array', + items: { $ref: getSchemaPath(ProductDTO) }, + }, + }, + }, + ], + }, }) - getProducts( + async getProducts( @Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number, @Query('limit', new DefaultValuePipe(10), ParseIntPipe) limit: number, + @Req() req: Request, ) { - return this.productsServices.getProducts(page, limit); + const baseUrl = this.configService.get('API_URL') + `${req.path}`; + const count = await this.productsServices.countProducts(); + const { next, previous } = getPaginationUrl(baseUrl, page, limit, count); + const products = await this.productsServices.getProducts(page, limit); + return { products, count, next, previous }; } } diff --git a/src/products/products.service.ts b/src/products/products.service.ts index 71aabb8..77846f5 100644 --- a/src/products/products.service.ts +++ b/src/products/products.service.ts @@ -6,29 +6,21 @@ import { Repository } from 'typeorm'; @Injectable() export class ProductsService { constructor( - @InjectRepository(Product) private productRepository: Repository, + @InjectRepository(Product) + private productRepository: Repository, ) {} - async getProducts( - page: number, - limit: number, - ): Promise<{ - totalItems: number; - totalPages: number; - currentPage: number; - products: Product[]; - }> { - const totalItems = await this.productRepository + async countProducts(): Promise { + return await this.productRepository .createQueryBuilder('product') .where('product.deletedAt IS NULL') .getCount(); + } - const totalPages = Math.ceil(totalItems / limit); - + async getProducts(page: number, limit: number): Promise { const products = await this.productRepository .createQueryBuilder('product') .leftJoinAndSelect('product.images', 'images') - .leftJoinAndSelect('product.lot', 'lot') .leftJoinAndSelect('product.manufacturer', 'manufacturer') .leftJoinAndSelect('product.categories', 'categories') .leftJoinAndSelect('product.presentations', 'productPresentation') @@ -36,18 +28,12 @@ export class ProductsService { .where('product.deletedAt IS NULL') .andWhere('manufacturer.deletedAt IS NULL') .andWhere('images.deletedAt IS NULL') - .andWhere('lot.deletedAt IS NULL') .andWhere('productPresentation.deletedAt IS NULL') .andWhere('presentation.deletedAt IS NULL') .skip((page - 1) * limit) .take(limit) .getMany(); - return { - totalItems, - totalPages, - currentPage: page, - products, - }; + return products; } } diff --git a/src/utils/dto/base.dto.ts b/src/utils/dto/base.dto.ts new file mode 100644 index 0000000..291b37a --- /dev/null +++ b/src/utils/dto/base.dto.ts @@ -0,0 +1,17 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class UUIDBaseDTO { + @ApiProperty({ example: '123e4567-e89b-12d3-a456-426614174006' }) + id: string; +} + +export class BaseDTO extends UUIDBaseDTO { + @ApiProperty() + createdAt: Date; + + @ApiProperty() + updatedAt: Date; + + @ApiProperty({ example: null }) + deletedAt: Date; +} diff --git a/src/utils/dto/pagination.dto.ts b/src/utils/dto/pagination.dto.ts new file mode 100644 index 0000000..37a1432 --- /dev/null +++ b/src/utils/dto/pagination.dto.ts @@ -0,0 +1,15 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class PaginationDTO { + @ApiProperty() + results: T[]; + + @ApiProperty() + count: number; + + @ApiProperty() + next: string | null; + + @ApiProperty() + previous: string | null; +} diff --git a/src/utils/pagination-urls.ts b/src/utils/pagination-urls.ts new file mode 100644 index 0000000..e9d7386 --- /dev/null +++ b/src/utils/pagination-urls.ts @@ -0,0 +1,22 @@ +type PaginationURL = { + next: string | null; + previous: string | null; +}; + +export function getPaginationUrl( + baseUrl: string, + page: number, + limit: number, + count: number, +): PaginationURL { + const startIndex = (page - 1) * limit; + const endIndex = page * limit; + + const next = + endIndex < count ? `${baseUrl}?page=${page + 1}&limit=${limit}` : null; + + const previous = + startIndex > 0 ? `${baseUrl}?page=${page - 1}&limit=${limit}` : null; + + return { next, previous }; +}