From e683127edf34347cbc70a44993f671fa9ef9a95d Mon Sep 17 00:00:00 2001 From: AlefrankM Date: Fri, 13 Jan 2023 16:39:34 -0400 Subject: [PATCH 1/2] refactor: get all account information --- src/account/repositories/account.repository.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/account/repositories/account.repository.ts b/src/account/repositories/account.repository.ts index 3d72a35..88b56d5 100644 --- a/src/account/repositories/account.repository.ts +++ b/src/account/repositories/account.repository.ts @@ -13,6 +13,16 @@ export class AccountRepository extends Repository { async findByAddress(address: string): Promise { const account = await this.createQueryBuilder('account') + .select([ + 'account.accountId', + 'account.address', + 'account.username', + 'account.createdAt', + 'account.updatedAt', + 'image.url', + 'image.cid', + ]) + .leftJoin('account.picture', 'image') .where('account.address = :address', { address: address.toLocaleLowerCase() }) .getOne(); @@ -23,14 +33,14 @@ export class AccountRepository extends Repository { async changeInformation(account: Account, editAccountDto: EditAccountDto): Promise { account.background = - account.background && + editAccountDto.background && ({ url: formatImageUrl(editAccountDto.background.url), cid: editAccountDto.background.cid, } as Image); account.picture = - account.background && + editAccountDto.picture && ({ url: formatImageUrl(editAccountDto.picture.url), cid: editAccountDto.picture.cid, From 70a6f6571b77b724285282253a3701d2e822df19 Mon Sep 17 00:00:00 2001 From: AlefrankM Date: Sun, 15 Jan 2023 15:26:44 -0400 Subject: [PATCH 2/2] feat: infinite scroll on items by account --- .../__tests__/account.controller.spec.ts | 10 +- src/account/controllers/account.controller.ts | 23 ++-- .../repositories/account.repository.ts | 20 ++-- src/account/services/account.service.ts | 37 +++++- src/auth/auth.module.ts | 6 +- src/auth/controllers/auth.controller.ts | 6 +- .../controllers/collection.controller.ts | 6 +- src/collections/dto/create-collection.dto.ts | 2 +- .../services/collection.service.ts | 4 +- src/items/__tests__/item.service.spec.ts | 52 --------- src/items/controllers/image.controller.ts | 2 +- src/items/repositories/history.repository.ts | 2 +- src/items/repositories/item.repository.ts | 108 ++++++++++++++++-- src/items/services/history.service.ts | 2 +- src/items/services/item.service.ts | 21 +--- src/items/services/pinata.service.ts | 4 +- src/items/services/voucher.service.ts | 4 +- 17 files changed, 185 insertions(+), 124 deletions(-) diff --git a/src/account/__tests__/account.controller.spec.ts b/src/account/__tests__/account.controller.spec.ts index 422d6bc..e5904f3 100644 --- a/src/account/__tests__/account.controller.spec.ts +++ b/src/account/__tests__/account.controller.spec.ts @@ -20,13 +20,13 @@ const collectionServiceMock = () => ({ }); const accountServiceMock = () => ({ - findByAddress: jest.fn(), + findCreatedItemsByAddress: jest.fn(), changeInformation: jest.fn(), }); describe('AccountController', () => { let controller: AccountsController; - let itemService; + let accountService; beforeEach(async () => { const module = await Test.createTestingModule({ @@ -39,7 +39,7 @@ describe('AccountController', () => { }).compile(); controller = await module.get(AccountsController); - itemService = await module.get(ItemService); + accountService = await module.get(AccountService); }); describe('When calling findItems function', () => { @@ -75,9 +75,9 @@ describe('AccountController', () => { ]; const mockData = expected.map((prop) => ({ ...prop })); - itemService.findByAddress.mockResolvedValue(mockData); + accountService.findCreatedItemsByAddress.mockResolvedValue(mockData); - const actual = await controller.findItems('123', { limit: 15, page: 1 }); + const actual = await controller.findCreatedItems('123', { limit: 15, page: 1 }); expect(actual[0].itemId).toEqual(expected[0].itemId); }); diff --git a/src/account/controllers/account.controller.ts b/src/account/controllers/account.controller.ts index 126820a..1172503 100644 --- a/src/account/controllers/account.controller.ts +++ b/src/account/controllers/account.controller.ts @@ -16,12 +16,21 @@ export class AccountsController { ) {} @IsAddressValid() - @Get('/:address/items') - findItems( - @Param('address') address: string, - @Query() paginationDto: PaginationDto - ): Promise { - return this.itemService.findByAddress(address, paginationDto); + @Get('/:address/items/created') + findCreatedItems(@Param('address') address: string, @Query() paginationDto: PaginationDto) { + return this.accountService.findCreatedItemsByAddress(address, paginationDto); + } + + @IsAddressValid() + @Get('/:address/items/owned') + ownedItems(@Param('address') address: string, @Query() paginationDto: PaginationDto) { + return this.accountService.findOwnedItemsByAddress(address, paginationDto); + } + + @IsAddressValid() + @Get('/:address/items/on-sale') + onSaleItems(@Param('address') address: string, @Query() paginationDto: PaginationDto) { + return this.accountService.findOnSaleItemsByAddress(address, paginationDto); } @IsAddressValid() @@ -42,7 +51,7 @@ export class AccountsController { @Param('address') address: string, @Query() paginationDto: PaginationDto ): Promise { - return this.itemService.findLikedByAddress(address, paginationDto); + return this.accountService.findLikedByAddress(address, paginationDto); } @IsAddressValid() diff --git a/src/account/repositories/account.repository.ts b/src/account/repositories/account.repository.ts index 88b56d5..c359783 100644 --- a/src/account/repositories/account.repository.ts +++ b/src/account/repositories/account.repository.ts @@ -12,19 +12,13 @@ export class AccountRepository extends Repository { } async findByAddress(address: string): Promise { - const account = await this.createQueryBuilder('account') - .select([ - 'account.accountId', - 'account.address', - 'account.username', - 'account.createdAt', - 'account.updatedAt', - 'image.url', - 'image.cid', - ]) - .leftJoin('account.picture', 'image') - .where('account.address = :address', { address: address.toLocaleLowerCase() }) - .getOne(); + const account = await this.findOne({ + select: { accountId: true, address: true, username: true }, + relations: { picture: true, background: true }, + where: { + address: address.toLocaleLowerCase(), + }, + }); if (!account) return; diff --git a/src/account/services/account.service.ts b/src/account/services/account.service.ts index b3de504..03ae120 100644 --- a/src/account/services/account.service.ts +++ b/src/account/services/account.service.ts @@ -1,4 +1,8 @@ import { Injectable } from '@nestjs/common'; +import { BusinessErrors } from '../../common/constants'; +import { PaginationDto } from '../../common/dto/pagination.dto'; +import { BusinessException } from '../../common/exceptions/exception-types'; +import { Item } from '../../items/entities/item.entity'; import { CollectionRepository } from '../../collections/repositories/collection.repository'; import { ItemRepository } from '../../items/repositories/item.repository'; import { EditAccountDto } from '../dto/edit-account.dto'; @@ -13,10 +17,41 @@ export class AccountService { private itemsRepository: ItemRepository ) {} - async findByAddress(address: string) { + async findAccountByAddress(address: string) { return this.accountRepository.findByAddress(address); } + async findCreatedItemsByAddress(address: string, paginationDto: PaginationDto) { + const account = await this.accountRepository.findByAddress(address); + + if (!account) throw new BusinessException(BusinessErrors.address_not_associated); + + return this.itemsRepository.findCreatedByAccount(account.accountId, paginationDto); + } + async findOnSaleItemsByAddress(address: string, paginationDto: PaginationDto) { + const account = await this.accountRepository.findByAddress(address); + + if (!account) throw new BusinessException(BusinessErrors.address_not_associated); + + return this.itemsRepository.findOnSaleByAccount(account.accountId, paginationDto); + } + + async findOwnedItemsByAddress(address: string, paginationDto: PaginationDto) { + const account = await this.accountRepository.findByAddress(address); + + if (!account) throw new BusinessException(BusinessErrors.address_not_associated); + + return this.itemsRepository.findOwnedByAccount(account.accountId, paginationDto); + } + + async findLikedByAddress(address: string, paginationDto: PaginationDto): Promise { + const account = await this.accountRepository.findByAddress(address); + + if (!account) throw new BusinessException(BusinessErrors.address_not_associated); + + return this.itemsRepository.findLikedByAccount(account.accountId, paginationDto); + } + async changeInformation(address: string, editAccountDto: EditAccountDto): Promise { const account = await this.accountRepository.findByAddress(address); diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts index 0ad93e2..bc32849 100644 --- a/src/auth/auth.module.ts +++ b/src/auth/auth.module.ts @@ -9,9 +9,9 @@ import { JwtModule } from '@nestjs/jwt'; import { JwtStrategy } from './strategies/jwt.strategy'; import { CacheModule, Module } from '@nestjs/common'; import { PassportModule } from '@nestjs/passport'; -import { AccountService } from 'src/account/services/account.service'; -import { CollectionRepository } from 'src/collections/repositories/collection.repository'; -import { ItemRepository } from 'src/items/repositories/item.repository'; +import { AccountService } from '../account/services/account.service'; +import { CollectionRepository } from '../collections/repositories/collection.repository'; +import { ItemRepository } from '../items/repositories/item.repository'; @Module({ imports: [ diff --git a/src/auth/controllers/auth.controller.ts b/src/auth/controllers/auth.controller.ts index 9ebeaca..27529ae 100644 --- a/src/auth/controllers/auth.controller.ts +++ b/src/auth/controllers/auth.controller.ts @@ -5,8 +5,8 @@ import { CredentialsResponseDto } from '../dto/credentials-response.dto'; import { Public } from '../../auth/decorators/public.decorator'; import { SigninRequestDto } from '../dto/signin-request.dto'; import { IsAddressValid } from '../decorators/address.decorator'; -import { AccountService } from 'src/account/services/account.service'; -import { Account } from 'src/config/entities.config'; +import { AccountService } from '../../account/services/account.service'; +import { Account } from '../../account/entities/account.entity'; @Controller('auth') export class AuthController { @@ -27,6 +27,6 @@ export class AuthController { @IsAddressValid() @Post('/authenticate') authenticate(@Body() authCredentialsDto: CredentialsRequestDto): Promise { - return this.accountService.findByAddress(authCredentialsDto.address); + return this.accountService.findAccountByAddress(authCredentialsDto.address); } } diff --git a/src/collections/controllers/collection.controller.ts b/src/collections/controllers/collection.controller.ts index 5cc6f90..19d251f 100644 --- a/src/collections/controllers/collection.controller.ts +++ b/src/collections/controllers/collection.controller.ts @@ -1,8 +1,8 @@ import { Body, Controller, Get, Param, Patch, Query } from '@nestjs/common'; import { Collection } from '../../collections/entities/collection.entity'; -import { NewestItemsRequestDto } from 'src/items/dto/newest-items-request.dto'; -import { ItemPaginationDto } from 'src/items/dto/pagination-request.dto'; -import { PriceRangeDto } from 'src/items/dto/price-range.dto'; +import { NewestItemsRequestDto } from '../../items/dto/newest-items-request.dto'; +import { ItemPaginationDto } from '../../items/dto/pagination-request.dto'; +import { PriceRangeDto } from '../../items/dto/price-range.dto'; import { Public } from '../../auth/decorators/public.decorator'; import { CollectionPaginationDto } from '../dto/collection-pagination-request.dto'; import { EditCollectionDto } from '../dto/edit-collection.dto'; diff --git a/src/collections/dto/create-collection.dto.ts b/src/collections/dto/create-collection.dto.ts index 0689f58..1844058 100644 --- a/src/collections/dto/create-collection.dto.ts +++ b/src/collections/dto/create-collection.dto.ts @@ -1,5 +1,5 @@ import { IsArray, IsNotEmpty, IsNumber, Max, MaxLength, Min, MinLength } from 'class-validator'; -import { ImageRequestDto } from 'src/items/dto/image-request.dto'; +import { ImageRequestDto } from '../../items/dto/image-request.dto'; import { Collection } from '../entities/collection.entity'; export class CreateCollectionDto { diff --git a/src/collections/services/collection.service.ts b/src/collections/services/collection.service.ts index 8d3b6a0..d09d8d5 100644 --- a/src/collections/services/collection.service.ts +++ b/src/collections/services/collection.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; -import { ItemPaginationDto } from 'src/items/dto/pagination-request.dto'; -import { PriceRangeDto } from 'src/items/dto/price-range.dto'; +import { ItemPaginationDto } from '../../items/dto/pagination-request.dto'; +import { PriceRangeDto } from '../../items/dto/price-range.dto'; import { AccountRepository } from '../../account/repositories/account.repository'; import { BusinessErrors } from '../../common/constants'; import { BusinessException } from '../../common/exceptions/exception-types'; diff --git a/src/items/__tests__/item.service.spec.ts b/src/items/__tests__/item.service.spec.ts index ea34f62..159f5e8 100644 --- a/src/items/__tests__/item.service.spec.ts +++ b/src/items/__tests__/item.service.spec.ts @@ -183,41 +183,6 @@ describe('ItemService', () => { }); }); - describe('When findByAddress function is called', () => { - describe('and the address exist', () => { - it('should return the expected item', async () => { - const account = { accountId: '456' } as Account; - const expected = items; - - accountRepository.findByAddress.mockResolvedValue({ ...account }); - - const mockedData = expected.map((prop) => ({ ...prop })); - itemRepository.findByAccount.mockResolvedValue(mockedData); - - const actual = await service.findByAddress('123', { limit: 15, page: 1 }); - - expect(actual).toEqual(expected); - }); - }); - - describe('and the address does not exist', () => { - it('should throw a BusinessException with expected message', async () => { - const unexistingAddress = '123'; - - accountRepository.findByAddress.mockResolvedValue(null); - - const exception = () => service.findByAddress(unexistingAddress, { limit: 15, page: 1 }); - - await expect(exception).rejects.toThrow(BusinessException); - await expect(exception).rejects.toEqual( - new BusinessException(BusinessErrors.address_not_associated) - ); - - expect(itemRepository.findByAccount).not.toHaveBeenCalled(); - }); - }); - }); - describe('When findPagination function is called', () => { it('should return a pagination object ', async () => { const expected = { @@ -281,23 +246,6 @@ describe('ItemService', () => { expect(actual).toEqual(expected); }); }); - - describe('and the address does not exist', () => { - it('should throw a BusinessException with expected message', async () => { - const unexistingAddress = '123'; - - accountRepository.findByAddress.mockResolvedValue(null); - - const exception = () => service.findByAddress(unexistingAddress, { limit: 15, page: 1 }); - - await expect(exception).rejects.toThrow(BusinessException); - await expect(exception).rejects.toEqual( - new BusinessException(BusinessErrors.file_extension_not_supported) - ); - - expect(itemRepository.createItem).not.toHaveBeenCalled(); - }); - }); }); describe('When listAnItem function is called', () => { diff --git a/src/items/controllers/image.controller.ts b/src/items/controllers/image.controller.ts index d5c0475..eb5b709 100644 --- a/src/items/controllers/image.controller.ts +++ b/src/items/controllers/image.controller.ts @@ -1,7 +1,7 @@ import { Body, Controller, Post, UploadedFile, UseInterceptors } from '@nestjs/common'; import { FileInterceptor } from '@nestjs/platform-express'; import { PinataService } from '../services/pinata.service'; -import { IpfsAttribute } from 'src/common/types/ipfs-attribute'; +import { IpfsAttribute } from '../../common/types/ipfs-attribute'; @Controller('images') export class ImagesController { diff --git a/src/items/repositories/history.repository.ts b/src/items/repositories/history.repository.ts index 51d6583..6fee017 100644 --- a/src/items/repositories/history.repository.ts +++ b/src/items/repositories/history.repository.ts @@ -1,5 +1,5 @@ import { Injectable } from '@nestjs/common'; -import { PaginationDto } from 'src/common/dto/pagination.dto'; +import { PaginationDto } from '../../common/dto/pagination.dto'; import { DataSource, Repository } from 'typeorm'; import { Account } from '../../config/entities.config'; import { History } from '../entities/history.entity'; diff --git a/src/items/repositories/item.repository.ts b/src/items/repositories/item.repository.ts index d17d2ce..7320b8c 100644 --- a/src/items/repositories/item.repository.ts +++ b/src/items/repositories/item.repository.ts @@ -144,16 +144,108 @@ export class ItemRepository extends Repository { .getMany(); } - async findByAccount(accountId: string, paginationDto: PaginationDto): Promise { + async findCreatedByAccount(accountId: string, paginationDto: PaginationDto) { const { limit, skip } = paginationDto; + const [items, count] = await this.findAndCount({ + relations: { + image: true, + owner: true, + author: true, + tags: true, + collection: true, + }, + select: { + image: { url: true }, + owner: { accountId: true, address: true }, + author: { accountId: true, address: true }, + tags: { name: true, id: true }, + collection: { id: true }, + }, + where: { + status: Not(ItemStatus.Draft), + author: new Account(accountId), + }, + order: { createdAt: 'DESC' }, + take: limit, + skip: skip, + }); - return this.getItemQueryBuilder() - .where('item.status != :status', { status: ItemStatus.Draft }) - .andWhere('(item.ownerId = :accountId OR item.authorId = :accountId)', { accountId }) - .orderBy('item.createdAt', 'DESC') - .limit(limit) - .skip(skip) - .getMany(); + return { + count, + items, + }; + } + + async findOwnedByAccount(accountId: string, paginationDto: PaginationDto) { + const { limit, skip } = paginationDto; + + const [items, count] = await this.findAndCount({ + relations: { + image: true, + owner: true, + author: true, + tags: true, + collection: true, + }, + select: { + image: { url: true }, + owner: { accountId: true, address: true }, + author: { accountId: true, address: true }, + tags: { name: true, id: true }, + collection: { id: true }, + }, + where: { + status: Not(ItemStatus.Draft), + author: new Account(accountId), + }, + order: { createdAt: 'DESC' }, + take: limit, + skip: skip, + }); + + return { + count, + items, + }; + } + + async findOnSaleByAccount(accountId: string, paginationDto: PaginationDto) { + const { limit, skip } = paginationDto; + + const [items, count] = await this.findAndCount({ + relations: { + image: true, + owner: true, + author: true, + tags: true, + collection: true, + }, + select: { + image: { url: true }, + owner: { accountId: true, address: true }, + author: { accountId: true, address: true }, + tags: { name: true, id: true }, + collection: { id: true }, + }, + where: [ + { + author: new Account(accountId), + status: ItemStatus.NotListed, + }, + { + owner: new Account(accountId), + status: ItemStatus.NotListed, + }, + ], + order: { createdAt: 'DESC' }, + take: limit, + skip: skip, + }); + + return { + count, + items, + }; } async findLikedByAccount(accountId: string, paginationDto: PaginationDto): Promise { diff --git a/src/items/services/history.service.ts b/src/items/services/history.service.ts index 1a2063a..4a493fb 100644 --- a/src/items/services/history.service.ts +++ b/src/items/services/history.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@nestjs/common'; -import { PaginationDto } from 'src/common/dto/pagination.dto'; +import { PaginationDto } from '../../common/dto/pagination.dto'; import { History } from '../entities/history.entity'; import { HistoryRepository } from '../repositories/history.repository'; diff --git a/src/items/services/item.service.ts b/src/items/services/item.service.ts index 825b781..0377fa3 100644 --- a/src/items/services/item.service.ts +++ b/src/items/services/item.service.ts @@ -1,8 +1,8 @@ import { Injectable } from '@nestjs/common'; -import { CreateCollectionDto } from 'src/collections/dto/create-collection.dto'; -import { Collection } from '../../collections/entities/collection.entity'; +import { CreateCollectionDto } from '../../collections/dto/create-collection.dto'; import { Account } from '../../account/entities/account.entity'; import { AccountRepository } from '../../account/repositories/account.repository'; +import { Collection } from '../../collections/entities/collection.entity'; import { CollectionRepository } from '../../collections/repositories/collection.repository'; import { BusinessErrors } from '../../common/constants'; import { BusinessException, NotFoundException } from '../../common/exceptions/exception-types'; @@ -24,7 +24,6 @@ import { HistoryRepository } from '../repositories/history.repository'; import { ItemLikeRepository } from '../repositories/item-like.repository'; import { ItemRepository } from '../repositories/item.repository'; import { VoucherRepository } from '../repositories/voucher.repository'; -import { PaginationDto } from 'src/common/dto/pagination.dto'; @Injectable() export class ItemService { @@ -57,22 +56,6 @@ export class ItemService { return item; } - async findByAddress(address: string, paginationDto: PaginationDto): Promise { - const account = await this.accountRepository.findByAddress(address); - - if (!account) throw new BusinessException(BusinessErrors.address_not_associated); - - return this.itemRepository.findByAccount(account.accountId, paginationDto); - } - - async findLikedByAddress(address: string, paginationDto: PaginationDto): Promise { - const account = await this.accountRepository.findByAddress(address); - - if (!account) throw new BusinessException(BusinessErrors.address_not_associated); - - return this.itemRepository.findLikedByAccount(account.accountId, paginationDto); - } - async findPriceRange(): Promise { return this.itemRepository.findPriceRange(); } diff --git a/src/items/services/pinata.service.ts b/src/items/services/pinata.service.ts index f4a833f..72918b1 100644 --- a/src/items/services/pinata.service.ts +++ b/src/items/services/pinata.service.ts @@ -7,8 +7,8 @@ import { BusinessException } from '../../common/exceptions/exception-types'; import { isValidExtension, isValidSize } from '../../common/utils/image-utils'; import { BusinessErrors } from '../../common/constants'; import { PinataResponse } from '../../common/types/pinata-response'; -import { IpfsAttribute } from 'src/common/types/ipfs-attribute'; -import { IpfsTrait } from 'src/common/types/ipfs-trait'; +import { IpfsAttribute } from '../../common/types/ipfs-attribute'; +import { IpfsTrait } from '../../common/types/ipfs-trait'; import { ReservedAttribute } from '../enums/reserved-attribute.enum'; @Injectable() diff --git a/src/items/services/voucher.service.ts b/src/items/services/voucher.service.ts index 069d007..371d0cc 100644 --- a/src/items/services/voucher.service.ts +++ b/src/items/services/voucher.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; -import { BusinessErrors } from 'src/common/constants'; -import { BusinessException } from 'src/common/exceptions/exception-types'; +import { BusinessErrors } from '../../common/constants'; +import { BusinessException } from '../../common/exceptions/exception-types'; import { VoucherRepository } from '../repositories/voucher.repository'; @Injectable()