From 9f6e76c3eee10026932fad8b56f8c40744acf02b Mon Sep 17 00:00:00 2001 From: AlefrankM Date: Fri, 13 Jan 2023 13:44:55 -0400 Subject: [PATCH 1/2] feat: change account information --- src/account/account.module.ts | 3 +- src/account/controllers/account.controller.ts | 31 +++++++++--- src/account/dto/edit-account.dto.ts | 23 +++++++++ src/account/entities/account.entity.ts | 22 ++++++++- .../repositories/account.repository.ts | 24 +++++++++- src/account/services/account.service.ts | 25 ++++++++++ src/auth/auth.module.ts | 6 +++ src/auth/controllers/auth.controller.ts | 8 ++-- src/auth/dto/credentials-response.dto.ts | 3 ++ src/auth/services/auth.service.ts | 2 +- .../controllers/collection.controller.ts | 3 +- src/common/utils/average-item-likes-utils.ts | 2 +- src/config/datasource.config.ts | 2 +- src/items/repositories/item.repository.ts | 15 ++++-- src/items/services/item.service.ts | 13 ++--- .../1673616966435-account-information.ts | 47 +++++++++++++++++++ 16 files changed, 202 insertions(+), 27 deletions(-) create mode 100644 src/account/dto/edit-account.dto.ts create mode 100644 src/account/services/account.service.ts create mode 100644 src/migrations/1673616966435-account-information.ts diff --git a/src/account/account.module.ts b/src/account/account.module.ts index 6b95614..9a02253 100644 --- a/src/account/account.module.ts +++ b/src/account/account.module.ts @@ -4,11 +4,12 @@ import { ItemModule } from '../items/item.module'; import { Module } from '@nestjs/common'; import { CollectionRepository } from '../collections/repositories/collection.repository'; import { CollectionService } from '../collections/services/collection.service'; +import { AccountService } from './services/account.service'; @Module({ imports: [ItemModule], controllers: [AccountsController], - providers: [AccountRepository, CollectionRepository, CollectionService], + providers: [AccountRepository, CollectionRepository, CollectionService, AccountService], exports: [AccountRepository], }) export class AccountModule {} diff --git a/src/account/controllers/account.controller.ts b/src/account/controllers/account.controller.ts index e7a6d62..126820a 100644 --- a/src/account/controllers/account.controller.ts +++ b/src/account/controllers/account.controller.ts @@ -1,17 +1,27 @@ -import { Controller, Get, Param } from '@nestjs/common'; +import { Controller, Get, Param, Query, Patch, Body } from '@nestjs/common'; +import { PaginationDto } from '../../common/dto/pagination.dto'; import { IsAddressValid } from '../../auth/decorators/address.decorator'; import { Collection } from '../../collections/entities/collection.entity'; import { CollectionService } from '../../collections/services/collection.service'; import { Item } from '../../items/entities/item.entity'; import { ItemService } from '../../items/services/item.service'; +import { EditAccountDto } from '../dto/edit-account.dto'; +import { AccountService } from '../services/account.service'; @Controller('accounts') export class AccountsController { - constructor(private itemService: ItemService, private collectionService: CollectionService) {} + constructor( + private itemService: ItemService, + private collectionService: CollectionService, + private accountService: AccountService + ) {} @IsAddressValid() @Get('/:address/items') - findItems(@Param('address') address: string): Promise { - return this.itemService.findByAddress(address); + findItems( + @Param('address') address: string, + @Query() paginationDto: PaginationDto + ): Promise { + return this.itemService.findByAddress(address, paginationDto); } @IsAddressValid() @@ -28,7 +38,16 @@ export class AccountsController { @IsAddressValid() @Get('/:address/liked-items') - findLikedItems(@Param('address') address: string): Promise { - return this.itemService.findLikedByAddress(address); + findLikedItems( + @Param('address') address: string, + @Query() paginationDto: PaginationDto + ): Promise { + return this.itemService.findLikedByAddress(address, paginationDto); + } + + @IsAddressValid() + @Patch('/:address') + changeInformation(@Param('address') address: string, @Body() editAccountDto: EditAccountDto) { + return this.accountService.changeInformation(address, editAccountDto); } } diff --git a/src/account/dto/edit-account.dto.ts b/src/account/dto/edit-account.dto.ts new file mode 100644 index 0000000..9d560cd --- /dev/null +++ b/src/account/dto/edit-account.dto.ts @@ -0,0 +1,23 @@ +import { IsOptional, IsString } from 'class-validator'; + +export class EditAccountDto { + @IsOptional() + background: { + url: string; + cid: string; + }; + + @IsOptional() + picture: { + url: string; + cid: string; + }; + + @IsOptional() + @IsString() + address: string; + + @IsOptional() + @IsString() + username: string; +} diff --git a/src/account/entities/account.entity.ts b/src/account/entities/account.entity.ts index d55948b..9b1728f 100644 --- a/src/account/entities/account.entity.ts +++ b/src/account/entities/account.entity.ts @@ -1,9 +1,10 @@ import { Item } from '../../items/entities/item.entity'; -import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm'; +import { Column, Entity, JoinColumn, OneToMany, OneToOne, PrimaryGeneratedColumn } from 'typeorm'; import { History } from '../../items/entities/history.entity'; import { ItemLike } from '../../items/entities/item-like.entity'; import { Voucher } from '../../items/entities/voucher.entity'; import { Collection } from '../../collections/entities/collection.entity'; +import { Image } from '../../items/entities/image.entity'; @Entity() export class Account { @@ -13,6 +14,9 @@ export class Account { @Column({ unique: true, nullable: false }) address: string; + @Column({ unique: true, nullable: true }) + username: string; + @OneToMany(() => Item, (item) => item.owner, { cascade: true }) items: Item[]; @@ -28,6 +32,22 @@ export class Account { @OneToMany(() => Collection, (table) => table.owner, { cascade: true }) collections: Collection[]; + @OneToOne(() => Image, { + onDelete: 'CASCADE', + orphanedRowAction: 'delete', + cascade: true, + }) + @JoinColumn({ name: 'backgroundId' }) + background: Image; + + @OneToOne(() => Image, { + onDelete: 'CASCADE', + orphanedRowAction: 'delete', + cascade: true, + }) + @JoinColumn({ name: 'pictureId' }) + picture: Image; + @Column() createdAt: Date; diff --git a/src/account/repositories/account.repository.ts b/src/account/repositories/account.repository.ts index d191b58..8ec974f 100644 --- a/src/account/repositories/account.repository.ts +++ b/src/account/repositories/account.repository.ts @@ -1,6 +1,9 @@ import { Account } from '../entities/account.entity'; import { DataSource, Repository } from 'typeorm'; import { Injectable } from '@nestjs/common'; +import { EditAccountDto } from '../dto/edit-account.dto'; +import { formatImageUrl } from 'src/common/utils/image-utils'; +import { Image } from 'src/config/entities.config'; @Injectable() export class AccountRepository extends Repository { @@ -10,7 +13,6 @@ export class AccountRepository extends Repository { async findByAddress(address: string): Promise { const account = await this.createQueryBuilder('account') - .select(['account.accountId', 'account.address']) .where('account.address = :address', { address: address.toLocaleLowerCase() }) .getOne(); @@ -18,4 +20,24 @@ export class AccountRepository extends Repository { return account; } + + async changeInformation(account: Account, editAccountDto: EditAccountDto): Promise { + account.background = + account.background && + ({ + url: formatImageUrl(editAccountDto.background.url), + cid: editAccountDto.background.cid, + } as Image); + + account.picture = + account.background && + ({ + url: formatImageUrl(editAccountDto.picture.url), + cid: editAccountDto.picture.cid, + } as Image); + + account.username = editAccountDto.username; + + return this.save(account); + } } diff --git a/src/account/services/account.service.ts b/src/account/services/account.service.ts new file mode 100644 index 0000000..b3de504 --- /dev/null +++ b/src/account/services/account.service.ts @@ -0,0 +1,25 @@ +import { Injectable } from '@nestjs/common'; +import { CollectionRepository } from '../../collections/repositories/collection.repository'; +import { ItemRepository } from '../../items/repositories/item.repository'; +import { EditAccountDto } from '../dto/edit-account.dto'; +import { Account } from '../entities/account.entity'; +import { AccountRepository } from '../repositories/account.repository'; + +@Injectable() +export class AccountService { + constructor( + private collectionRepository: CollectionRepository, + private accountRepository: AccountRepository, + private itemsRepository: ItemRepository + ) {} + + async findByAddress(address: string) { + return this.accountRepository.findByAddress(address); + } + + async changeInformation(address: string, editAccountDto: EditAccountDto): Promise { + const account = await this.accountRepository.findByAddress(address); + + return this.accountRepository.changeInformation(account, editAccountDto); + } +} diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts index 64b3395..0ad93e2 100644 --- a/src/auth/auth.module.ts +++ b/src/auth/auth.module.ts @@ -9,6 +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'; @Module({ imports: [ @@ -38,6 +41,9 @@ import { PassportModule } from '@nestjs/passport'; provide: APP_GUARD, useClass: AddressGuard, }, + AccountService, + CollectionRepository, + ItemRepository, ], exports: [AuthService], controllers: [AuthController], diff --git a/src/auth/controllers/auth.controller.ts b/src/auth/controllers/auth.controller.ts index e905a66..9ebeaca 100644 --- a/src/auth/controllers/auth.controller.ts +++ b/src/auth/controllers/auth.controller.ts @@ -5,10 +5,12 @@ 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'; @Controller('auth') export class AuthController { - constructor(private authService: AuthService) {} + constructor(private authService: AuthService, private accountService: AccountService) {} @Public() @Post('/signin') @@ -24,7 +26,7 @@ export class AuthController { @IsAddressValid() @Post('/authenticate') - authenticate(@Body() authCredentialsDto: CredentialsRequestDto): string { - return authCredentialsDto.address; + authenticate(@Body() authCredentialsDto: CredentialsRequestDto): Promise { + return this.accountService.findByAddress(authCredentialsDto.address); } } diff --git a/src/auth/dto/credentials-response.dto.ts b/src/auth/dto/credentials-response.dto.ts index 74122e9..3b6636d 100644 --- a/src/auth/dto/credentials-response.dto.ts +++ b/src/auth/dto/credentials-response.dto.ts @@ -1,3 +1,6 @@ +import { Account } from 'src/config/entities.config'; + export class CredentialsResponseDto { access_token: string; + account: Account; } diff --git a/src/auth/services/auth.service.ts b/src/auth/services/auth.service.ts index 74f149f..6303d8e 100644 --- a/src/auth/services/auth.service.ts +++ b/src/auth/services/auth.service.ts @@ -46,7 +46,7 @@ export class AuthService { this.cacheManager.del(address); - return { access_token }; + return { access_token, account }; } throw new UnauthorizedException(); diff --git a/src/collections/controllers/collection.controller.ts b/src/collections/controllers/collection.controller.ts index b3cc814..5cc6f90 100644 --- a/src/collections/controllers/collection.controller.ts +++ b/src/collections/controllers/collection.controller.ts @@ -1,5 +1,5 @@ import { Body, Controller, Get, Param, Patch, Query } from '@nestjs/common'; -import { Collection } from 'src/config/entities.config'; +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'; @@ -51,7 +51,6 @@ export class CollectionsController { @Param('collectionId') collectionId: string, @Body() editCollectionDto: EditCollectionDto ): Promise { - console.log(editCollectionDto); return this.collectionService.changeInformation(collectionId, editCollectionDto); } } diff --git a/src/common/utils/average-item-likes-utils.ts b/src/common/utils/average-item-likes-utils.ts index a22bc2d..fa8fca4 100644 --- a/src/common/utils/average-item-likes-utils.ts +++ b/src/common/utils/average-item-likes-utils.ts @@ -1,4 +1,4 @@ -import { Item } from 'src/config/entities.config'; +import { Item } from '../../items/entities/item.entity'; export const getAverage = (items: Item[]) => { const itemsTotalLikes = items.reduce((prev, actual) => prev + actual.likes, 0); diff --git a/src/config/datasource.config.ts b/src/config/datasource.config.ts index 1a24617..e0ee6ef 100644 --- a/src/config/datasource.config.ts +++ b/src/config/datasource.config.ts @@ -11,7 +11,7 @@ export function getConfig() { username: process.env.DB_USERNAME, password: process.env.DB_PASSWORD, database: process.env.DB_NAME, - migrations: ['src/migrations/**/*{.js,.ts}'], + migrations: ['src/migrations/*{.js,.ts}'], entities: [__dirname + '/entities.config.ts'], extra: { ssl: { diff --git a/src/items/repositories/item.repository.ts b/src/items/repositories/item.repository.ts index b78ae52..d17d2ce 100644 --- a/src/items/repositories/item.repository.ts +++ b/src/items/repositories/item.repository.ts @@ -1,7 +1,6 @@ import { Injectable } from '@nestjs/common'; -import { formatImageUrl } from '../../common/utils/image-utils'; +import { PaginationDto } from '../../common/dto/pagination.dto'; import { - Between, DataSource, FindManyOptions, FindOptionsWhere, @@ -12,6 +11,7 @@ import { } from 'typeorm'; import { Account } from '../../account/entities/account.entity'; import { Collection } from '../../collections/entities/collection.entity'; +import { formatImageUrl } from '../../common/utils/image-utils'; import { DraftItemRequestDto } from '../dto/draft-item-request.dto'; import { ItemRequestDto } from '../dto/item-request.dto'; import { LazyItemRequestDto } from '../dto/lazy-item-request.dto'; @@ -144,20 +144,27 @@ export class ItemRepository extends Repository { .getMany(); } - async findByAccount(accountId: string): Promise { + async findByAccount(accountId: string, paginationDto: PaginationDto): Promise { + const { limit, skip } = paginationDto; + 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(); } - async findLikedByAccount(accountId: string): Promise { + async findLikedByAccount(accountId: string, paginationDto: PaginationDto): Promise { + const { limit, skip } = paginationDto; return this.getItemQueryBuilder() .innerJoin('item.itemLikes', 'itemLikes') .where('itemLikes.accountId = :accountId', { accountId }) .andWhere('item.status != :status', { status: ItemStatus.Draft }) .orderBy('item.createdAt', 'DESC') + .limit(limit) + .skip(skip) .getMany(); } diff --git a/src/items/services/item.service.ts b/src/items/services/item.service.ts index 1d25860..825b781 100644 --- a/src/items/services/item.service.ts +++ b/src/items/services/item.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { CreateCollectionDto } from 'src/collections/dto/create-collection.dto'; -import { Collection } from 'src/config/entities.config'; +import { Collection } from '../../collections/entities/collection.entity'; import { Account } from '../../account/entities/account.entity'; import { AccountRepository } from '../../account/repositories/account.repository'; import { CollectionRepository } from '../../collections/repositories/collection.repository'; @@ -24,6 +24,7 @@ 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 { @@ -56,20 +57,20 @@ export class ItemService { return item; } - async findByAddress(address: string): Promise { + 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); + return this.itemRepository.findByAccount(account.accountId, paginationDto); } - async findLikedByAddress(address: string): Promise { + 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); + return this.itemRepository.findLikedByAccount(account.accountId, paginationDto); } async findPriceRange(): Promise { @@ -202,7 +203,7 @@ export class ItemService { } async like(itemId: string, address: string): Promise { - const itemToLike = await this.itemRepository.findActiveById(itemId); + const itemToLike = await this.itemRepository.findById(itemId); const history = await this.historyRepository.findAllByItemId(itemId); if (!itemToLike) throw new NotFoundException(`The item with id ${itemId} does not exist`); diff --git a/src/migrations/1673616966435-account-information.ts b/src/migrations/1673616966435-account-information.ts new file mode 100644 index 0000000..6a4a96f --- /dev/null +++ b/src/migrations/1673616966435-account-information.ts @@ -0,0 +1,47 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class accountInformation1673616966435 implements MigrationInterface { + name = 'accountInformation1673616966435'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "account" ADD "username" character varying`); + await queryRunner.query( + `ALTER TABLE "account" ADD CONSTRAINT "UQ_41dfcb70af895ddf9a53094515b" UNIQUE ("username")` + ); + await queryRunner.query(`ALTER TABLE "account" ADD "backgroundId" uuid`); + await queryRunner.query( + `ALTER TABLE "account" ADD CONSTRAINT "UQ_907e58534f4a89c8b0c95514d3f" UNIQUE ("backgroundId")` + ); + await queryRunner.query(`ALTER TABLE "account" ADD "pictureId" uuid`); + await queryRunner.query( + `ALTER TABLE "account" ADD CONSTRAINT "UQ_abb1452444a28137c45db08906e" UNIQUE ("pictureId")` + ); + await queryRunner.query( + `ALTER TABLE "account" ADD CONSTRAINT "FK_907e58534f4a89c8b0c95514d3f" FOREIGN KEY ("backgroundId") REFERENCES "image"("imageId") ON DELETE CASCADE ON UPDATE NO ACTION` + ); + await queryRunner.query( + `ALTER TABLE "account" ADD CONSTRAINT "FK_abb1452444a28137c45db08906e" FOREIGN KEY ("pictureId") REFERENCES "image"("imageId") ON DELETE CASCADE ON UPDATE NO ACTION` + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "account" DROP CONSTRAINT "FK_abb1452444a28137c45db08906e"` + ); + await queryRunner.query( + `ALTER TABLE "account" DROP CONSTRAINT "FK_907e58534f4a89c8b0c95514d3f"` + ); + await queryRunner.query( + `ALTER TABLE "account" DROP CONSTRAINT "UQ_abb1452444a28137c45db08906e"` + ); + await queryRunner.query(`ALTER TABLE "account" DROP COLUMN "pictureId"`); + await queryRunner.query( + `ALTER TABLE "account" DROP CONSTRAINT "UQ_907e58534f4a89c8b0c95514d3f"` + ); + await queryRunner.query(`ALTER TABLE "account" DROP COLUMN "backgroundId"`); + await queryRunner.query( + `ALTER TABLE "account" DROP CONSTRAINT "UQ_41dfcb70af895ddf9a53094515b"` + ); + await queryRunner.query(`ALTER TABLE "account" DROP COLUMN "username"`); + } +} From adb27f58fe8e79f1992fa0a227dac2b8275f34d0 Mon Sep 17 00:00:00 2001 From: AlefrankM Date: Fri, 13 Jan 2023 14:10:22 -0400 Subject: [PATCH 2/2] refactor: updating tests --- src/account/__tests__/account.controller.spec.ts | 9 ++++++++- src/account/repositories/account.repository.ts | 4 ++-- src/items/__tests__/item.service.spec.ts | 6 +++--- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/account/__tests__/account.controller.spec.ts b/src/account/__tests__/account.controller.spec.ts index f415434..422d6bc 100644 --- a/src/account/__tests__/account.controller.spec.ts +++ b/src/account/__tests__/account.controller.spec.ts @@ -7,6 +7,7 @@ import { Collection, Image } from '../../config/entities.config'; import { ItemService } from '../../items/services/item.service'; import { Voucher } from '../../items/entities/voucher.entity'; import { CollectionService } from '../../collections/services/collection.service'; +import { AccountService } from '../services/account.service'; const itemServiceMock = () => ({ findByAddress: jest.fn(), @@ -18,6 +19,11 @@ const collectionServiceMock = () => ({ findByOwner: jest.fn(), }); +const accountServiceMock = () => ({ + findByAddress: jest.fn(), + changeInformation: jest.fn(), +}); + describe('AccountController', () => { let controller: AccountsController; let itemService; @@ -28,6 +34,7 @@ describe('AccountController', () => { providers: [ { provide: ItemService, useFactory: itemServiceMock }, { provide: CollectionService, useFactory: collectionServiceMock }, + { provide: AccountService, useFactory: accountServiceMock }, ], }).compile(); @@ -70,7 +77,7 @@ describe('AccountController', () => { const mockData = expected.map((prop) => ({ ...prop })); itemService.findByAddress.mockResolvedValue(mockData); - const actual = await controller.findItems('123'); + const actual = await controller.findItems('123', { limit: 15, page: 1 }); expect(actual[0].itemId).toEqual(expected[0].itemId); }); diff --git a/src/account/repositories/account.repository.ts b/src/account/repositories/account.repository.ts index 8ec974f..3d72a35 100644 --- a/src/account/repositories/account.repository.ts +++ b/src/account/repositories/account.repository.ts @@ -2,8 +2,8 @@ import { Account } from '../entities/account.entity'; import { DataSource, Repository } from 'typeorm'; import { Injectable } from '@nestjs/common'; import { EditAccountDto } from '../dto/edit-account.dto'; -import { formatImageUrl } from 'src/common/utils/image-utils'; -import { Image } from 'src/config/entities.config'; +import { formatImageUrl } from '../../common/utils/image-utils'; +import { Image } from '../../items/entities/image.entity'; @Injectable() export class AccountRepository extends Repository { diff --git a/src/items/__tests__/item.service.spec.ts b/src/items/__tests__/item.service.spec.ts index f7e1a64..ea34f62 100644 --- a/src/items/__tests__/item.service.spec.ts +++ b/src/items/__tests__/item.service.spec.ts @@ -194,7 +194,7 @@ describe('ItemService', () => { const mockedData = expected.map((prop) => ({ ...prop })); itemRepository.findByAccount.mockResolvedValue(mockedData); - const actual = await service.findByAddress('123'); + const actual = await service.findByAddress('123', { limit: 15, page: 1 }); expect(actual).toEqual(expected); }); @@ -206,7 +206,7 @@ describe('ItemService', () => { accountRepository.findByAddress.mockResolvedValue(null); - const exception = () => service.findByAddress(unexistingAddress); + const exception = () => service.findByAddress(unexistingAddress, { limit: 15, page: 1 }); await expect(exception).rejects.toThrow(BusinessException); await expect(exception).rejects.toEqual( @@ -288,7 +288,7 @@ describe('ItemService', () => { accountRepository.findByAddress.mockResolvedValue(null); - const exception = () => service.findByAddress(unexistingAddress); + const exception = () => service.findByAddress(unexistingAddress, { limit: 15, page: 1 }); await expect(exception).rejects.toThrow(BusinessException); await expect(exception).rejects.toEqual(